Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/cn/php/phpy.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@ composer require swoole/phpy

`clear-cache`命令会根据`phpy.json`的`config.cache-dir`清除相关缓存,更多查看`--help`

### 7. 切换pip镜像
```shell
./vendor/bin/phpy config:pip-mirror
```

`config:pip-mirror`预设部分pip镜像源提供选择变更,并且支持自定义pip镜像源,更多查看`--help`

## 共建维护

### 公共映射库
Expand Down
83 changes: 83 additions & 0 deletions tests/phpunit/tools/phpy/ConfigTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace tools\phpy;

use PhpyTool\Phpy\Config;
use PHPUnit\Framework\TestCase;

class ConfigTest extends TestCase
{
public function testSetAndGetNestedKeys()
{
$config = new Config();
// 测试新键创建
$config->set('a.b.c', 'value');
$this->assertEquals('value', $config->get('a.b.c'));

// 测试覆盖现有值
$config->set('python.source-url', 'new_url');
$this->assertEquals('new_url', $config->get('python.source-url'));
}

public function testGetWithDefault()
{
$config = new Config();
$this->assertNull($config->get('non.exist.key'));
$this->assertEquals('default', $config->get('non.exist.key', 'default'));
}

public function testMergeConfigs()
{
$config1 = new Config();
$config1->set('config.scan-dirs', ['/path1']);

$config2 = new Config();
$config2->set('config.scan-dirs', ['/path2']);

$config1->merge($config2);
$this->assertEquals(
['/path1', '/path2'],
$config1->get('config.scan-dirs')
);
}

public function testAllMethod()
{
$config = new Config();
// transform=true时modules转stdClass
$result = $config->all();
$this->assertInstanceOf(\stdClass::class, $result['modules']);

// transform=false时保持原类型
$result = $config->all(false);
$this->assertIsArray($result['modules']);
}

public function testToStringSerialization()
{
$config = new Config();
$jsonString = (string)$config;

// 验证基础结构
$this->assertStringContainsString('"python": {', $jsonString);
$this->assertStringContainsString('"modules": {}', $jsonString);

// 验证格式选项
$this->assertStringNotContainsString('\\/', $jsonString); // 验证JSON_UNESCAPED_SLASHES
$this->assertMatchesRegularExpression('/"install-dir":\s+"\/usr"/', $jsonString); // 验证缩进
}

public function testModulesHandling()
{
$config = new Config();
// 测试空数组转换
$config->set('modules', []);
$this->assertInstanceOf(\stdClass::class, $config->all()['modules']);

// 测试非空数组保持原样
$config->set('modules', ['test']);
$this->assertIsArray($config->all()['modules']);
}
}
45 changes: 45 additions & 0 deletions tests/phpunit/tools/phpy/VersionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace tools\phpy;

use PhpyTool\Phpy\Exceptions\PhpyException;
use PhpyTool\Phpy\Helpers\Process;
use PhpyTool\Phpy\Helpers\Version;
use PHPUnit\Framework\TestCase;

class VersionTest extends TestCase
{
public function testPepToSemverConversion()
{
// Test epoch and post-release
$this->assertNull(Version::pepToSemver('1!2.3.4'));
$this->assertNull(Version::pepToSemver('1.2.3.post4'));

// Test version normalization
$this->assertEquals('1.2.0', Version::pepToSemver('v1.2'));
$this->assertEquals('1.2.34', Version::pepToSemver('1.2.3.4'));

// Test pre-release labels
$this->assertEquals('1.2.0-alpha.1', Version::pepToSemver('1.2a1'));
$this->assertEquals('1.2.0-beta.2', Version::pepToSemver('1.2b2'));
$this->assertEquals('1.2.0-rc.3', Version::pepToSemver('1.2rc3'));
$this->assertNull(Version::pepToSemver('1.2dev4'));
}

public function testValidatePepVersion()
{
$this->assertTrue(Version::validatePepVersion('1.2.3'));
$this->assertFalse(Version::validatePepVersion('invalid-version'));
$this->assertTrue(Version::validatePepVersion('1.2.3-beta.4', false));
}

public function testSplitVersion()
{
$this->assertEquals([1, 2, 3], Version::splitVersion('v1.2.3'));
$this->assertEquals([1, 2, 0], Version::splitVersion('1.2'));
$this->assertEquals([1, 0, 0], Version::splitVersion('1'));
$this->assertEquals([2, 3, 4], Version::splitVersion('2.3.4.5.6'));
}
}
13 changes: 8 additions & 5 deletions tools/src/Phpy/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@
use PhpyTool\Phpy\Commands\MetadataPushCommand;
use PhpyTool\Phpy\Commands\PythonInstall;
use PhpyTool\Phpy\Commands\ScanCommand;
use PhpyTool\Phpy\Commands\ScanImport;
use PhpyTool\Phpy\Commands\ShowCommand;
use PhpyTool\Phpy\Commands\UpdateCommand;
use PhpyTool\Phpy\Helpers\System;

class Application extends \Symfony\Component\Console\Application
{
/** @var string */
public const VERSION = '0.0.1';
public const VERSION = '0.1.0';

/** @var string */
private string $logo = <<<doc
Expand All @@ -39,18 +38,22 @@ public function __construct()
System::setcwd(getcwd());
parent::__construct('PHPy', static::VERSION);
$this->addCommands([
// 共建资源相关
new MetadataQueryCommand(),
new MetadataPushCommand(),
new ScanCommand(),
// 配置相关
new InitConfigCommand(),
new PipMirrorConfig(),
// phpy工具相关
new InstallCommand(),
new UpdateCommand(),
new ScanCommand(),
new ShowCommand(),
new ClearCacheCommand(),
// 独立安装命令
new PipModuleInstall(),
new PhpyInstall(),
new PythonInstall(),
new PipMirrorConfig(),
new ClearCacheCommand(),
]);
}

Expand Down
2 changes: 1 addition & 1 deletion tools/src/Phpy/Commands/ClearCacheCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected function handler(): int
if (!$this->consoleIO?->ask(
"<info>No phpy.json in current directory, do you want to use the one at $cDir</info> [<comment>Y,n</comment>]?",
true,
ConfirmationQuestion::class
questionClass: ConfirmationQuestion::class
)) {
throw new CommandStopException("PHPy could not find a phpy.json file in $sDir");
}
Expand Down
2 changes: 1 addition & 1 deletion tools/src/Phpy/Commands/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ protected function handler(): int
if (!$this->consoleIO?->ask(
"<info>No phpy.json in current directory, do you want to use the one at $cDir</info> [<comment>Y,n</comment>]?",
true,
ConfirmationQuestion::class
questionClass: ConfirmationQuestion::class
)) {
throw new CommandStopException("PHPy could not find a phpy.json file in $sDir");
}
Expand Down
109 changes: 74 additions & 35 deletions tools/src/Phpy/Commands/PipMirrorConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,96 @@

namespace PhpyTool\Phpy\Commands;

use Symfony\Component\Console\Helper\QuestionHelper;
use PhpyTool\Phpy\Helpers\System;
use Symfony\Component\Console\Question\ChoiceQuestion;

class PipMirrorConfig extends AbstractCommand
{

/**
* 预设的 pip 镜像源
*
* @var array|string[]
*/
protected static array $mirrors = [
'Python官方' => 'https://pypi.org/simple/',
'清华' => 'https://pypi.tuna.tsinghua.edu.cn/simple',
'阿里云' => 'http://mirrors.aliyun.com/pypi/simple/',
'华为云' => 'https://mirrors.huaweicloud.com/repository/pypi/simple/',
'腾讯云' => 'https://mirrors.cloud.tencent.com/pypi/simple/',
'中科大' => 'https://pypi.mirrors.ustc.edu.cn/simple/',
'网易' => 'http://mirrors.163.com/pypi/simple/',
];

/**
* 设置 pip 镜像源
*
* @param string $name
* @param string $url
* @return bool
*/
public static function addMirror(string $name, string $url): bool
{
if (!static::$mirrors[$name] ?? null) {
static::$mirrors[$name] = $url;
return true;
}
return false;
}

/** @inheritdoc */
protected function configure(): void
{
parent::configure();
$this
->setName('config:pip-mirror')
->setDescription('Set pip mirror');
}
->setDescription('Set pip mirror')
->setHelp(<<<EOT
该命令预设部分pip镜像源提供选择变更,并且支持自定义pip镜像源;

protected function execPipCommand(string $pipCommand): string
{
if ($this->process->executePip($pipCommand, $output) != 0) {
throw new \RuntimeException('Failed to execute pip command');
}
return implode("\n", $output);
<comment>自定义pip镜像源</comment>需在当前项目根目录创建<info>pip-mirror.json</info>,运行时自动加载;
<info>pip-mirror.json</info>格式样例:
<info>
{
"Python官方": "https://pypi.org/simple/",
"中科大": "https://pypi.mirrors.ustc.edu.cn/simple/"
}
</info>
<comment>自定义pip镜像源</comment>运行时自动加载。
EOT
);
}

/** @inheritdoc */
protected function handler(): int
{
$this->consoleIO?->output('Current pip mirror url: ' . $this->execPipCommand('config get global.index-url'));
$this->consoleIO?->output('Select the pip mirror source ...');
$options = ['Python 官方', '清华', '阿里云', '华为云', '腾讯云', '中科大', '网易'];

// 创建选择问题
$question = new ChoiceQuestion(
'请选择 pip 镜像站:',
$options,
0
);

$question->setErrorMessage('Invalid option.');

$helper = new QuestionHelper();
$selectedOption = $helper->ask($this->consoleIO?->getInput(), $this->consoleIO?->getOutput(), $question);

match ($selectedOption) {
'Python 官方' => $this->execPipCommand('config set global.index-url https://pypi.org/simple/'),
'清华' => $this->execPipCommand('config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple'),
'阿里云' => $this->execPipCommand('config set global.index-url http://mirrors.aliyun.com/pypi/simple/'),
'华为云' => $this->execPipCommand('config set global.index-url https://mirrors.huaweicloud.com/repository/pypi/simple/'),
'腾讯云' => $this->execPipCommand('config set global.index-url https://mirrors.cloud.tencent.com/pypi/simple/'),
'中科大' => $this->execPipCommand('config set global.index-url https://pypi.mirrors.ustc.edu.cn/simple/'),
'网易' => $this->execPipCommand('config set global.index-url http://mirrors.163.com/pypi/simple/'),
};

return $this->consoleIO?->success('已将 pip 源设置为: ' . $selectedOption);
// 加载自定义配置文件
$config = System::getcwd(). '/pip-mirror.json';
if (file_exists($config)) {
try {
$config = json_decode(trim(System::getFileContent($config)), true, flags: JSON_THROW_ON_ERROR);
if ($config) {
foreach ($config as $name => $url) {
if (filter_var($url, FILTER_VALIDATE_URL)) {
static::addMirror($name, $url);
}
}
}
} catch (\JsonException) {}
}
// 选择镜像源
$options = array_keys(static::$mirrors);
$selectedOption = $this->consoleIO?->choice('请选择 pip 镜像站:', $options, 0, function (ChoiceQuestion $choice) {
$choice->setErrorMessage('Invalid option.');
});
$indexUrl = static::$mirrors[$selectedOption] ?? null;
if (!$indexUrl) {
return $this->consoleIO?->error('Invalid option.');
}
if ($this->process->executePip("config set global.index-url $indexUrl", subOutput: true) !== 0) {
return $this->consoleIO?->error('Failed to set pip mirror.');
}
return $this->consoleIO?->success("已将 pip 源设置为: $selectedOption -> $indexUrl");
}
}
2 changes: 1 addition & 1 deletion tools/src/Phpy/Commands/ScanCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ protected function handler(): int
if (!$this->consoleIO?->ask(
"<info>No phpy.json in current directory, do you want to use the one at $cDir</info> [<comment>Y,n</comment>]?",
true,
ConfirmationQuestion::class
questionClass: ConfirmationQuestion::class
)) {
throw new CommandStopException("PHPy could not find a phpy.json file in $sDir");
}
Expand Down
2 changes: 1 addition & 1 deletion tools/src/Phpy/Commands/UpdateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ protected function handler(): int
if (!$this->consoleIO?->ask(
"<info>No phpy.json in current directory, do you want to use the one at $cDir</info> [<comment>Y,n</comment>]?",
true,
ConfirmationQuestion::class
questionClass: ConfirmationQuestion::class
)) {
throw new CommandStopException("PHPy could not find a phpy.json file in $sDir");
}
Expand Down
8 changes: 4 additions & 4 deletions tools/src/Phpy/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ class Config
*/
public function __toString(): string
{
$this->config['modules'] = $this->config['modules'] ?: new \stdClass();
return json_encode($this->config, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
return json_encode($this->all(), JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}

/**
Expand Down Expand Up @@ -146,9 +145,10 @@ public function set(string $key, mixed $value): void
*/
public function all(bool $transform = true): array
{
$config = $this->config;
if ($transform) {
$this->config['modules'] = $this->config['modules'] ?: new \stdClass();
$config['modules'] = $config['modules'] ?: new \stdClass();
}
return $this->config;
return $config;
}
}
Loading