From b323de794ab3cfd38ac50bb8f9faa859cd2e8055 Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Tue, 1 Apr 2025 16:32:03 +0800 Subject: [PATCH 01/11] 1. fix Version::getPepVersions retry did not meet expectations 2. fix ask by ConfirmationQuestion tag format error --- tools/src/Phpy/Commands/ClearCacheCommand.php | 2 +- tools/src/Phpy/Commands/InstallCommand.php | 2 +- tools/src/Phpy/Commands/ScanCommand.php | 2 +- tools/src/Phpy/Commands/UpdateCommand.php | 2 +- tools/src/Phpy/Helpers/Process.php | 1 + tools/src/Phpy/Helpers/System.php | 7 ++----- tools/src/Phpy/Helpers/Version.php | 14 ++++++++------ 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tools/src/Phpy/Commands/ClearCacheCommand.php b/tools/src/Phpy/Commands/ClearCacheCommand.php index 5dfa046..d494a9b 100644 --- a/tools/src/Phpy/Commands/ClearCacheCommand.php +++ b/tools/src/Phpy/Commands/ClearCacheCommand.php @@ -44,7 +44,7 @@ protected function handler(): int if (!$this->consoleIO?->ask( "No phpy.json in current directory, do you want to use the one at $cDir [Y,n]?", true, - ConfirmationQuestion::class + questionClass: ConfirmationQuestion::class )) { throw new CommandStopException("PHPy could not find a phpy.json file in $sDir"); } diff --git a/tools/src/Phpy/Commands/InstallCommand.php b/tools/src/Phpy/Commands/InstallCommand.php index 134475b..6b7bd72 100644 --- a/tools/src/Phpy/Commands/InstallCommand.php +++ b/tools/src/Phpy/Commands/InstallCommand.php @@ -65,7 +65,7 @@ protected function handler(): int if (!$this->consoleIO?->ask( "No phpy.json in current directory, do you want to use the one at $cDir [Y,n]?", true, - ConfirmationQuestion::class + questionClass: ConfirmationQuestion::class )) { throw new CommandStopException("PHPy could not find a phpy.json file in $sDir"); } diff --git a/tools/src/Phpy/Commands/ScanCommand.php b/tools/src/Phpy/Commands/ScanCommand.php index 46ed8c5..fca4032 100644 --- a/tools/src/Phpy/Commands/ScanCommand.php +++ b/tools/src/Phpy/Commands/ScanCommand.php @@ -46,7 +46,7 @@ protected function handler(): int if (!$this->consoleIO?->ask( "No phpy.json in current directory, do you want to use the one at $cDir [Y,n]?", true, - ConfirmationQuestion::class + questionClass: ConfirmationQuestion::class )) { throw new CommandStopException("PHPy could not find a phpy.json file in $sDir"); } diff --git a/tools/src/Phpy/Commands/UpdateCommand.php b/tools/src/Phpy/Commands/UpdateCommand.php index 2fd5c8f..a4846dd 100644 --- a/tools/src/Phpy/Commands/UpdateCommand.php +++ b/tools/src/Phpy/Commands/UpdateCommand.php @@ -54,7 +54,7 @@ protected function handler(): int if (!$this->consoleIO?->ask( "No phpy.json in current directory, do you want to use the one at $cDir [Y,n]?", true, - ConfirmationQuestion::class + questionClass: ConfirmationQuestion::class )) { throw new CommandStopException("PHPy could not find a phpy.json file in $sDir"); } diff --git a/tools/src/Phpy/Helpers/Process.php b/tools/src/Phpy/Helpers/Process.php index b6874f1..6769abc 100644 --- a/tools/src/Phpy/Helpers/Process.php +++ b/tools/src/Phpy/Helpers/Process.php @@ -132,6 +132,7 @@ public function executePythonConfig(string $command, ?array &$output = null, boo /** * 请求 + * - 当未安装curl拓展时尝试使用curl命令执行http请求 * * @param string $method * @param string $url diff --git a/tools/src/Phpy/Helpers/System.php b/tools/src/Phpy/Helpers/System.php index 01bd24d..1e7c498 100644 --- a/tools/src/Phpy/Helpers/System.php +++ b/tools/src/Phpy/Helpers/System.php @@ -22,7 +22,7 @@ class System /** * @return false|string */ - public static function getcwd() + public static function getcwd(): bool|string { return $GLOBALS['PHPY_CWD'] ?? getcwd(); } @@ -31,7 +31,7 @@ public static function getcwd() * @param string $cwd * @return void */ - public static function setcwd(string $cwd) + public static function setcwd(string $cwd): void { $GLOBALS['PHPY_CWD'] = $cwd; } @@ -52,7 +52,6 @@ public static function python(?string $path = null): string throw new PhpyException('Python not found. '); } } -// System::putFileContent($command, $path); return $path; } @@ -72,7 +71,6 @@ public static function pip(?string $path = null): string throw new PhpyException('Python-pip not found. '); } } -// System::putFileContent($command, $path); return $path; } @@ -92,7 +90,6 @@ public static function pythonConfig(?string $path = null): string throw new PhpyException('Python-config not found. '); } } -// System::putFileContent($command, $path); return $path; } diff --git a/tools/src/Phpy/Helpers/Version.php b/tools/src/Phpy/Helpers/Version.php index c2d4470..44a297c 100644 --- a/tools/src/Phpy/Helpers/Version.php +++ b/tools/src/Phpy/Helpers/Version.php @@ -21,7 +21,7 @@ public static function getPepVersions(string $module): array static $modulePepVersions = []; if (!isset($modulePepVersions[$module])) { $retry = 0; - do { + while (1) { try { $res = (new Process())->request( 'GET', @@ -31,17 +31,19 @@ public static function getPepVersions(string $module): array 'Content-Type' => 'application/json' ] ); - } catch (\Throwable) { - echo "request error, retry $retry"; + break; + } catch (\Throwable $throwable) { + if ($retry > 2) { + throw new PhpyException("Request failed: {$throwable->getMessage()}"); + } usleep(($retry + 1 ) * 200 * 1000); } finally { $retry ++; } - } while ($retry < 3); - + } $httpCode = $res['httpCode'] ?? 500; - $res = json_decode($responseBody = $res['responseBody'], true); + $res = json_decode(($responseBody = $res['responseBody'] ?? ''), true); if ($httpCode === 404) { $modulePepVersions[$module] = []; } else if ($httpCode === 200) { From ef4aa362cd3e2cf4d62add318cd53b05463ed750 Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Tue, 1 Apr 2025 16:33:06 +0800 Subject: [PATCH 02/11] 1. optimized config:pip-mirror command --- tools/src/Phpy/Commands/PipMirrorConfig.php | 80 ++++++++++++--------- 1 file changed, 45 insertions(+), 35 deletions(-) diff --git a/tools/src/Phpy/Commands/PipMirrorConfig.php b/tools/src/Phpy/Commands/PipMirrorConfig.php index 14538d4..e7ff2e3 100644 --- a/tools/src/Phpy/Commands/PipMirrorConfig.php +++ b/tools/src/Phpy/Commands/PipMirrorConfig.php @@ -4,11 +4,42 @@ namespace PhpyTool\Phpy\Commands; -use Symfony\Component\Console\Helper\QuestionHelper; 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 { @@ -18,43 +49,22 @@ protected function configure(): void ->setDescription('Set pip mirror'); } - 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); - } - /** @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); + $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"); } } From b12c5d4a0e046fb3a0ddccf8363e4a4bbd3e21cd Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Tue, 1 Apr 2025 16:34:02 +0800 Subject: [PATCH 03/11] 1. add ConsoleIO::choice --- tools/src/Phpy/ConsoleIO.php | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tools/src/Phpy/ConsoleIO.php b/tools/src/Phpy/ConsoleIO.php index a5593be..ca7be8b 100644 --- a/tools/src/Phpy/ConsoleIO.php +++ b/tools/src/Phpy/ConsoleIO.php @@ -4,6 +4,7 @@ namespace PhpyTool\Phpy; +use Closure; use PhpyTool\Phpy\Helpers\Process; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Formatter\OutputFormatterStyle; @@ -12,9 +13,8 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\Question; -use Symfony\Component\Console\Style\SymfonyStyle; class ConsoleIO { @@ -131,6 +131,9 @@ public function getExtra(?string $key = null, mixed $default = null): mixed */ public function ask(string $message, mixed $default = null, string $tag = '[?]', string $questionClass = Question::class): mixed { + if (!is_a($questionClass, Question::class)) { + throw new \InvalidArgumentException("$questionClass is not a valid Question class"); + } // 询问安装目录 $question = new $questionClass("$tag $message \n", $default); /** @var QuestionHelper $questionHelper */ @@ -138,6 +141,22 @@ public function ask(string $message, mixed $default = null, string $tag = '[?]', return $questionHelper->ask($this->getInput(), $this->getOutput(), $question); } + /** + * @param string $message + * @param array $choices + * @param mixed|null $default + * @param Closure|null $closure = function (ChoiceQuestion $question) {} + * @param string $tag + * @return mixed + */ + public function choice(string $message, array $choices, mixed $default = null, ?Closure $closure = null, string $tag = '[?]'): mixed + { + $question = new ChoiceQuestion("$tag $message \n", $choices, $default); + /** @var QuestionHelper $questionHelper */ + $questionHelper = $this->getHelperSet()->get('question'); + return $questionHelper->ask($this->getInput(), $this->getOutput(), $question); + } + /** * sub输出 * From b539b1c604d267107def4187cc156b75bd1799ae Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Tue, 1 Apr 2025 18:22:27 +0800 Subject: [PATCH 04/11] 1. add config:pip-mirror supports pip-mirror.json --- tools/src/Phpy/Commands/PipMirrorConfig.php | 33 +++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/tools/src/Phpy/Commands/PipMirrorConfig.php b/tools/src/Phpy/Commands/PipMirrorConfig.php index e7ff2e3..8707183 100644 --- a/tools/src/Phpy/Commands/PipMirrorConfig.php +++ b/tools/src/Phpy/Commands/PipMirrorConfig.php @@ -4,6 +4,7 @@ namespace PhpyTool\Phpy\Commands; +use PhpyTool\Phpy\Helpers\System; use Symfony\Component\Console\Question\ChoiceQuestion; class PipMirrorConfig extends AbstractCommand @@ -46,14 +47,42 @@ protected function configure(): void parent::configure(); $this ->setName('config:pip-mirror') - ->setDescription('Set pip mirror'); + ->setDescription('Set pip mirror') + ->setHelp(<<自定义pip镜像源需在当前项目根目录创建pip-mirror.json,运行时自动加载; +pip-mirror.json格式样例: + +{ + "Python官方": "https://pypi.org/simple/", + "中科大": "https://pypi.mirrors.ustc.edu.cn/simple/" +} + +自定义pip镜像源运行时自动加载。 +EOT + ); } /** @inheritdoc */ protected function handler(): int { - $this->consoleIO?->output('Select the pip mirror source ...'); + // 加载自定义配置文件 + $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.'); From 0938f4c87b52467005a1f9d44d8d33a8c4d91916 Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Tue, 1 Apr 2025 18:25:53 +0800 Subject: [PATCH 05/11] version update --- tools/src/Phpy/Application.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tools/src/Phpy/Application.php b/tools/src/Phpy/Application.php index 854191a..96f6588 100644 --- a/tools/src/Phpy/Application.php +++ b/tools/src/Phpy/Application.php @@ -15,7 +15,6 @@ 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; @@ -23,7 +22,7 @@ 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 = <<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(), ]); } From b84a548657ce04ceb3d518fde605face6552ddd7 Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Wed, 2 Apr 2025 13:42:55 +0800 Subject: [PATCH 06/11] doc update --- docs/cn/php/phpy.md | 7 +++++++ tools/src/Phpy/Helpers/Process.php | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/cn/php/phpy.md b/docs/cn/php/phpy.md index 50073e0..d70aa88 100644 --- a/docs/cn/php/phpy.md +++ b/docs/cn/php/phpy.md @@ -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` + ## 共建维护 ### 公共映射库 diff --git a/tools/src/Phpy/Helpers/Process.php b/tools/src/Phpy/Helpers/Process.php index 6769abc..9732972 100644 --- a/tools/src/Phpy/Helpers/Process.php +++ b/tools/src/Phpy/Helpers/Process.php @@ -57,6 +57,11 @@ private function debugMode(string $info): void /** * 执行命令 + * - 默认将stderr重定向合并至stdout + * - command支持使用2>,自定义stderr重定向 + * - 2>/dev/null: 忽略stderr + * - 2>&1: 合并stderr至stdout + * - 2>/tmp/run.log: 将stderr重定向至文件 * * @param string $command 执行命令 * @param array|null $output stdout & stderr (结果列表始终为倒序) @@ -65,7 +70,7 @@ private function debugMode(string $info): void */ public function execute(string $command, ?array &$output = null, bool $subOutput = false): int { - $command = str_ends_with($command, ' 2>&1') ? $command : "$command 2>&1"; + $command = str_contains($command, ' 2>') ? $command : "$command 2>&1"; $this->debugMode("execute( $command )"); $resultCode = -1; $output = $output === null ? [] : $output; From 49a2b83f7a283d5ca4ccf533994d6ed6b8e4c5ae Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Wed, 2 Apr 2025 15:38:36 +0800 Subject: [PATCH 07/11] add PHPy Helpers/Version test --- tests/phpunit/tools/phpy/VersionTest.php | 81 +++++++++++++++++++++ tools/src/Phpy/Helpers/PackageCollector.php | 14 ++++ 2 files changed, 95 insertions(+) create mode 100644 tests/phpunit/tools/phpy/VersionTest.php diff --git a/tests/phpunit/tools/phpy/VersionTest.php b/tests/phpunit/tools/phpy/VersionTest.php new file mode 100644 index 0000000..d9bffe0 --- /dev/null +++ b/tests/phpunit/tools/phpy/VersionTest.php @@ -0,0 +1,81 @@ +getMockBuilder(Process::class) + ->onlyMethods(['request']) + ->getMock(); + + // Test normal response + $processMock->expects($this->exactly(2)) + ->method('request') + ->willReturnOnConsecutiveCalls( + ['httpCode' => 200, 'responseBody' => json_encode(['releases' => ['1.0.0', '1.1.0']])], + ['httpCode' => 404, 'responseBody' => ''] + ); + + $this->assertEquals(['1.0.0', '1.1.0'], Version::getPepVersions('valid-module')); + $this->assertEquals([], Version::getPepVersions('not-found-module')); + } + + 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')); + } + + public function testGetPepVersionsRetryLogic() + { + $processMock = $this->getMockBuilder(Process::class) + ->onlyMethods(['request']) + ->getMock(); + + $processMock->expects($this->exactly(3)) + ->method('request') + ->willThrowException(new \Exception('Timeout')); + + $this->expectException(PhpyException::class); + Version::getPepVersions('failing-module'); + } +} diff --git a/tools/src/Phpy/Helpers/PackageCollector.php b/tools/src/Phpy/Helpers/PackageCollector.php index 64d2e9f..1271eea 100644 --- a/tools/src/Phpy/Helpers/PackageCollector.php +++ b/tools/src/Phpy/Helpers/PackageCollector.php @@ -10,8 +10,15 @@ class PackageCollector extends NodeVisitorAbstract { + /** + * @var array + */ private array $packages = []; + /** + * @param Node $node + * @return void + */ public function enterNode(Node $node): void { $foundPackageFn = function (Node $node, int $index = 0) { @@ -44,11 +51,18 @@ public function enterNode(Node $node): void } } + /** + * @return array + */ public function getPackages(): array { return array_unique($this->packages); // 去重 } + /** + * @param $filePath + * @return array + */ static function parseFile($filePath): array { $code = file_get_contents($filePath); From 801d9e83527bbdb3b742a5b3babf1b8a298bd98c Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Wed, 2 Apr 2025 16:17:56 +0800 Subject: [PATCH 08/11] update --- tools/src/Phpy/ConsoleIO.php | 2 +- tools/src/Phpy/Installers/ModuleInstaller.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/src/Phpy/ConsoleIO.php b/tools/src/Phpy/ConsoleIO.php index ca7be8b..8635a62 100644 --- a/tools/src/Phpy/ConsoleIO.php +++ b/tools/src/Phpy/ConsoleIO.php @@ -131,7 +131,7 @@ public function getExtra(?string $key = null, mixed $default = null): mixed */ public function ask(string $message, mixed $default = null, string $tag = '[?]', string $questionClass = Question::class): mixed { - if (!is_a($questionClass, Question::class)) { + if (!is_a($questionClass, Question::class, true)) { throw new \InvalidArgumentException("$questionClass is not a valid Question class"); } // 询问安装目录 diff --git a/tools/src/Phpy/Installers/ModuleInstaller.php b/tools/src/Phpy/Installers/ModuleInstaller.php index b1e6feb..60d3f0a 100644 --- a/tools/src/Phpy/Installers/ModuleInstaller.php +++ b/tools/src/Phpy/Installers/ModuleInstaller.php @@ -67,7 +67,7 @@ public function scan(): void $this->consoleIO?->output("Scanning $file"); $scannedPackages = PackageCollector::parseFile($file); foreach ($scannedPackages as $package) { - $this->consoleIO?->subOutput("-- scanned: $package"); + $this->consoleIO?->subOutput("-- scanned top_level: $package"); } $packages = array_merge($packages, $scannedPackages); } @@ -108,9 +108,9 @@ public function scan(): void } if ($modules) { $count = count($modules); - $this->consoleIO?->output("Scanned: $count"); + $this->consoleIO?->output("Scanned modules: $count"); foreach ($modules as $module => $version) { - $this->consoleIO?->subOutput("-- $module - $version"); + $this->consoleIO?->subOutput("-- module_name: $module - $version"); } } if (!$this->consoleIO?->ask( From dc5a757805488c5f641d77e9d69b8476d1d0659e Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Wed, 2 Apr 2025 16:27:35 +0800 Subject: [PATCH 09/11] VersionTest update --- tests/phpunit/tools/phpy/VersionTest.php | 36 ------------------------ 1 file changed, 36 deletions(-) diff --git a/tests/phpunit/tools/phpy/VersionTest.php b/tests/phpunit/tools/phpy/VersionTest.php index d9bffe0..5f48765 100644 --- a/tests/phpunit/tools/phpy/VersionTest.php +++ b/tests/phpunit/tools/phpy/VersionTest.php @@ -11,28 +11,6 @@ class VersionTest extends TestCase { - /** - * Test getPepVersions with different HTTP responses - */ - public function testGetPepVersions() - { - // Mock Process class with different responses - $processMock = $this->getMockBuilder(Process::class) - ->onlyMethods(['request']) - ->getMock(); - - // Test normal response - $processMock->expects($this->exactly(2)) - ->method('request') - ->willReturnOnConsecutiveCalls( - ['httpCode' => 200, 'responseBody' => json_encode(['releases' => ['1.0.0', '1.1.0']])], - ['httpCode' => 404, 'responseBody' => ''] - ); - - $this->assertEquals(['1.0.0', '1.1.0'], Version::getPepVersions('valid-module')); - $this->assertEquals([], Version::getPepVersions('not-found-module')); - } - public function testPepToSemverConversion() { // Test epoch and post-release @@ -64,18 +42,4 @@ public function testSplitVersion() $this->assertEquals([1, 0, 0], Version::splitVersion('1')); $this->assertEquals([2, 3, 4], Version::splitVersion('2.3.4.5.6')); } - - public function testGetPepVersionsRetryLogic() - { - $processMock = $this->getMockBuilder(Process::class) - ->onlyMethods(['request']) - ->getMock(); - - $processMock->expects($this->exactly(3)) - ->method('request') - ->willThrowException(new \Exception('Timeout')); - - $this->expectException(PhpyException::class); - Version::getPepVersions('failing-module'); - } } From 6731f161f8d5db74d76cff786b5f859ecb69a1d2 Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Wed, 2 Apr 2025 16:39:46 +0800 Subject: [PATCH 10/11] ConfigTest update --- tests/phpunit/tools/phpy/ConfigTest.php | 83 +++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/phpunit/tools/phpy/ConfigTest.php diff --git a/tests/phpunit/tools/phpy/ConfigTest.php b/tests/phpunit/tools/phpy/ConfigTest.php new file mode 100644 index 0000000..1f3437f --- /dev/null +++ b/tests/phpunit/tools/phpy/ConfigTest.php @@ -0,0 +1,83 @@ +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']); + } +} From a7fc6fdf2a033563c26bf8e4a26aee88773a7891 Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Wed, 2 Apr 2025 16:45:54 +0800 Subject: [PATCH 11/11] fix Config --- tools/src/Phpy/Config.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/src/Phpy/Config.php b/tools/src/Phpy/Config.php index 34cf5bc..7ccdff7 100644 --- a/tools/src/Phpy/Config.php +++ b/tools/src/Phpy/Config.php @@ -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); } /** @@ -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; } }