fix(python): retain facade types without phpy loaded (#128) --skip-tests

master
yavon007 3 weeks ago committed by GitHub
parent c7cdc8b23e
commit 69e2a29ec2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 24
      phpunit/code/python/facade-class-name-collision.php
  2. 14
      phpunit/code/python/facade-methods.php
  3. 29
      phpunit/src/Python/PythonModuleTest.php
  4. 63
      phpunit/src/Python/PythonWithoutExtensionTest.php
  5. 6
      src/CompilerBase.php
  6. 2
      src/Context/CompilationStateTrait.php
  7. 123
      src/Python/PythonModuleTrait.php
  8. 42
      tests/compiler/python/facade-methods.phpt

@ -0,0 +1,24 @@
<?php
class PyObject
{
public function toValue(): int
{
return 42;
}
}
class PyList extends PyObject
{
public function toArray(): array
{
return [1, 2];
}
}
function main(): void
{
$value = new PyObject();
$list = new PyList();
var_dump($value->toValue(), $list->toArray());
}

@ -0,0 +1,14 @@
<?php
function main(): void
{
$list = python\list([1, 2, 3]);
$count = $list->count();
$contains = $list->contains(2);
$slice = $list->slice(0, 2)->toArray();
$set = python\set([1, 2]);
$member = $set->contains(1);
$value = python\int(42);
$value = $list;
var_dump($count, $contains, $slice, $member, $value->toArray());
}

@ -348,6 +348,35 @@ final class PythonModuleTest extends TestCase
$this->assertStringNotContainsString('phpy::', $cpp);
}
public function testPythonFacadeMethodsKeepTheirPhpReturnTypes(): void
{
global $translator;
$translator = $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/python/facade-methods.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$cpp = file_get_contents($compiler->convertFile($source));
self::assertStringContainsString('php::Int count = 0;', $cpp);
self::assertStringContainsString('php::Bool contains = 0;', $cpp);
self::assertStringContainsString('php::Bool member = 0;', $cpp);
self::assertStringContainsString('php::Array slice;', $cpp);
self::assertStringContainsString('php::python::toArray(', $cpp);
self::assertStringNotContainsString('php::python::callMember(', $cpp);
}
public function testProjectClassesWithFacadeNamesDoNotUseThePythonBridge(): void
{
global $translator;
$translator = $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/python/facade-class-name-collision.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$cpp = file_get_contents($compiler->convertFile($source));
self::assertStringNotContainsString('php::python::', $cpp);
}
private function compileFixture(string $file): void
{
global $translator;

@ -0,0 +1,63 @@
<?php
namespace TypePhp\Tests\Python;
use PHPUnit\Framework\TestCase;
final class PythonWithoutExtensionTest extends TestCase
{
public function testPythonCodeGenerationDoesNotRequirePhpy(): void
{
// CI normally loads phpy. Start a clean compiler host to keep this contract covered.
$command = [PHP_BINARY, '-n'];
foreach (['ctype', 'mbstring', 'tokenizer'] as $extension) {
$path = ini_get('extension_dir') . DIRECTORY_SEPARATOR
. (PHP_OS_FAMILY === 'Windows' ? 'php_' : '') . $extension . '.' . PHP_SHLIB_SUFFIX;
if (is_file($path)) {
$command[] = '-d';
$command[] = 'extension=' . $path;
}
}
$command[] = '-r';
$command[] = <<<'PHP'
if (extension_loaded('phpy')) {
throw new RuntimeException('The compiler host must not load phpy');
}
require 'phpunit/bootstrap.php';
$result = [];
foreach (['operators', 'object-protocol', 'object-conversion-methods', 'facade-methods', 'facade-class-name-collision'] as $fixture) {
$translator = TypePhp\CompilerTest::create(TYPEPHP_ROOT_PATH);
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/python/' . $fixture . '.php';
$translator->addFiles([$source]);
$translator->prepareFile($source);
$result[$fixture] = file_get_contents($translator->convertFile($source))
. file_get_contents($translator->genExtension());
}
echo "\nTYPEPHP_RESULT:" . json_encode($result, JSON_THROW_ON_ERROR);
PHP;
$process = proc_open($command, [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
], $pipes, TYPEPHP_ROOT_PATH);
self::assertIsResource($process);
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[2]);
self::assertSame(0, proc_close($process), $stdout . $stderr);
self::assertSame(1, preg_match('/\nTYPEPHP_RESULT:(.*)\z/s', $stdout, $matches), $stdout);
$generated = json_decode($matches[1], true, flags: JSON_THROW_ON_ERROR);
self::assertStringNotContainsString('php::python::', $generated['facade-class-name-collision']);
self::assertStringContainsString('iadd', $generated['operators']);
self::assertStringContainsString('is_not', $generated['operators']);
self::assertStringContainsString('php::python::call(', $generated['object-protocol']);
self::assertStringContainsString('php::python::toArray(', $generated['object-conversion-methods']);
self::assertStringNotContainsString('php::toArray(', $generated['object-conversion-methods']);
self::assertStringContainsString('php::Var value;', $generated['object-conversion-methods']);
self::assertStringContainsString('php::python::toArray(', $generated['facade-methods']);
self::assertStringNotContainsString('php::python::callMember(', $generated['facade-methods']);
}
}

@ -2596,6 +2596,12 @@ class CompilerBase implements PropertyAccessContext
return true;
}
// Python facade inheritance is a compiler-known bridge contract and
// does not depend on phpy being loaded in the compiler process.
if ($this->isPythonFacadeAssignableTo($class, $expected)) {
return true;
}
if (!$this->hasClass($class)
&& !$this->hasInterface($class)
&& !$this->isInternalClass($class)

@ -258,7 +258,7 @@ trait CompilationStateTrait
// Interfaces have no concrete method body for native calls. Abstract classes may have concrete methods.
if ($this->isInterface($class)) {
$this->context->declaredObjects[$name] = $class;
} elseif ($this->isNativeClass($class) or $this->isInternalClass($class)) {
} elseif ($this->isNativeClass($class) || $this->isInternalClass($class) || $this->isPythonFacadeClass($class)) {
$this->context->objects[$name] = $class;
}
}

@ -9,6 +9,51 @@ use TypePhp\Type;
trait PythonModuleTrait
{
/** phpy facade inheritance is part of the Python language bridge, even without the extension loaded. */
private const PYTHON_FACADE_PARENTS = [
'pyobject' => '',
'pysequence' => 'pyobject',
'pylist' => 'pysequence',
'pytuple' => 'pysequence',
'pydict' => 'pyobject',
'pyset' => 'pyobject',
'pystr' => 'pyobject',
'pytype' => 'pyobject',
'pyfn' => 'pyobject',
'pyiter' => 'pyobject',
'pymodule' => 'pyobject',
];
/**
* Public method contracts from phpy's stubs/phpy_{object,sequence,set}.stub.php.
* These Zend methods must not become Python attribute calls when phpy is absent.
*/
private const PYTHON_FACADE_METHOD_RETURN_TYPES = [
'pyobject' => [
'__construct' => Type::VOID,
'__call' => Type::VAR,
'__get' => Type::VAR,
'__set' => Type::VOID,
'__unset' => Type::VOID,
'__tostring' => Type::STR,
'toarray' => Type::ARRAY,
'tovalue' => Type::VAR,
'__invoke' => Type::VAR,
'offsetget' => Type::VAR,
'offsetset' => Type::VOID,
'offsetunset' => Type::VOID,
'offsetexists' => Type::BOOL,
'key' => Type::VAR,
'next' => Type::VOID,
'rewind' => Type::VOID,
'valid' => Type::BOOL,
'current' => Type::VAR,
'count' => Type::INT,
],
'pysequence' => ['contains' => Type::BOOL, 'slice' => Type::OBJECT],
'pyset' => ['contains' => Type::BOOL],
];
/** @var array<string, string> TypePHP constructor sugar to the existing phpy facade class. */
private const PYTHON_CONSTRUCTOR_CLASSES = [
'list' => 'PyList',
@ -56,6 +101,47 @@ trait PythonModuleTrait
protected bool $pythonRuntimeUsed = false;
protected function isPythonFacadeClass(string $class): bool
{
$class = ltrim($class, '\\');
return isset(self::PYTHON_FACADE_PARENTS[strtolower($class)])
&& !$this->hasClass($class)
&& !$this->hasInterface($class);
}
protected function isPythonFacadeAssignableTo(string $class, string $expected): bool
{
if (!$this->isPythonFacadeClass($class) || !$this->isPythonFacadeClass($expected)) {
return false;
}
$class = strtolower(ltrim($class, '\\'));
$expected = strtolower(ltrim($expected, '\\'));
do {
if ($class === $expected) {
return true;
}
$class = self::PYTHON_FACADE_PARENTS[$class];
} while ($class !== '');
return false;
}
private function getPythonFacadeMethodReturnType(string $class, string $method): ?string
{
if (!$this->isPythonFacadeClass($class)) {
return null;
}
$method = strtolower($method);
$class = strtolower(ltrim($class, '\\'));
do {
$type = self::PYTHON_FACADE_METHOD_RETURN_TYPES[$class][$method] ?? null;
if ($type !== null) {
return $type;
}
$class = self::PYTHON_FACADE_PARENTS[$class];
} while ($class !== '');
return null;
}
/** Return true when the expression is statically known to hold a phpy proxy. */
protected function isPythonObjectExpr(NodeAbstract $expr): bool
{
@ -63,8 +149,12 @@ trait PythonModuleTrait
if ($class === '') {
return false;
}
return strcasecmp($class, 'PyObject') === 0
|| $this->isObjectClassStaticallyAssignableTo($class, 'PyObject');
if ($this->isPythonFacadeClass($class)) {
return true;
}
return !$this->hasClass('PyObject')
&& !$this->hasInterface('PyObject')
&& $this->isObjectClassStaticallyAssignableTo($class, 'PyObject');
}
/**
@ -79,6 +169,9 @@ trait PythonModuleTrait
}
$class = $this->detectClassOfExpr($receiver);
if ($this->getPythonFacadeMethodReturnType($class, $method) !== null) {
return false;
}
return $class === '' || !\TypePhp\Resolver\Reflection::hasMethod($class, $method);
}
@ -87,17 +180,17 @@ trait PythonModuleTrait
if (!$this->isNamedMethod($expr->name) || !$this->isPythonObjectExpr($expr->var)) {
return null;
}
$method = $expr->name->toString();
$method = strtolower($expr->name->toString());
$helper = match ($method) {
'toValue' => 'toValue',
'toArray' => 'toArray',
'tovalue' => 'toValue',
'toarray' => 'toArray',
default => null,
};
if ($helper === null) {
return null;
}
if ($expr->args !== []) {
$this->fatalError($expr, "The {$method} method does not accept parameters");
$this->fatalError($expr, "The {$helper} method does not accept parameters");
}
$this->markPythonRuntimeUsed();
return 'php::python::' . $helper . '(' . $receiver . ')';
@ -571,6 +664,15 @@ trait PythonModuleTrait
return Type::OBJECT;
}
if ($expr instanceof Expr\MethodCall && $this->isPythonObjectExpr($expr->var)) {
if ($this->isIdExpr($expr->name)) {
$facadeType = $this->getPythonFacadeMethodReturnType(
$this->detectClassOfExpr($expr->var),
$this->parseIdentifier($expr->name),
);
if ($facadeType !== null) {
return $facadeType;
}
}
if (!$this->isIdExpr($expr->name)
|| $this->isPythonDynamicMethodCall($expr->var, $this->parseIdentifier($expr->name))
) {
@ -613,6 +715,15 @@ trait PythonModuleTrait
return 'PyObject';
}
if ($expr instanceof Expr\MethodCall && $this->isPythonObjectExpr($expr->var)) {
if ($this->isIdExpr($expr->name)) {
$facadeType = $this->getPythonFacadeMethodReturnType(
$this->detectClassOfExpr($expr->var),
$this->parseIdentifier($expr->name),
);
if ($facadeType !== null) {
return $facadeType === Type::OBJECT ? 'PyObject' : '';
}
}
if (!$this->isIdExpr($expr->name)
|| $this->isPythonDynamicMethodCall($expr->var, $this->parseIdentifier($expr->name))
) {

@ -0,0 +1,42 @@
--TEST--
Python facade methods retain their PHP return types and inherited behavior
--SKIPIF--
<?php
if (!extension_loaded('phpy')) {
die('skip phpy extension is not loaded');
}
?>
--FILE--
<?php
function main(): void
{
$list = python\list([1, 2, 3]);
$count = $list->count();
$contains = $list->contains(2);
$slice = $list->slice(0, 2)->toArray();
$set = python\set([1, 2]);
$member = $set->contains(1);
$value = python\int(42);
$value = $list;
var_dump($count, $contains, $slice, $member, $value->toArray());
}
?>
--EXPECT--
int(3)
bool(true)
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
bool(true)
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}
Loading…
Cancel
Save