aotuai/example-cython-poetry-pypi

Doesn't work with Cython3

Opened this issue · 4 comments

Thank you for the tutorial and the repo. I've verified that the project does work with cython 0.x series but fails on cython3.

$  /tmp/example-cython-poetry-pypi 
 ❯ poetry build -vvv                                                                                                                                                                                                               
Using virtualenv: /Users/lmwangi/Library/Caches/pypoetry/virtualenvs/poetry-pypi-example-wSzIWKA2-py3.9
Building poetry-pypi-example (0.0.1)
  - Building sdist
  - Adding: /private/tmp/example-cython-poetry-pypi/poetry_pypi_example/main.cpython-39-darwin.so
  - Adding: README.rst
  - Adding: pyproject.toml
  - Built poetry-pypi-example-0.0.1.tar.gz
  - Building wheel
  - Executing build script: build.py
[1/1] Cythonizing poetry_pypi_example/main.py
Traceback (most recent call last):
  File "/private/tmp/example-cython-poetry-pypi/build.py", line 80, in <module>
    build()
  File "/private/tmp/example-cython-poetry-pypi/build.py", line 30, in build
    build_ext_cmd.copy_extensions_to_source()
  File "/Users/lmwangi/Library/Caches/pypoetry/virtualenvs/poetry-pypi-example-wSzIWKA2-py3.9/lib/python3.9/site-packages/setuptools/_distutils/cmd.py", line 103, in __getattr__
    raise AttributeError(attr)
AttributeError: copy_extensions_to_source

On drilling down, it looks like the import for build_ext in cython3 does not have ** copy_extensions_to_source**. To be precise in line 14, build_ext is None when it is imported from setuptools. Please see below:

Python 3.9.13 (main, May 24 2022, 21:28:31)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.6.0 -- An enhanced Interactive Python. Type '?' for help.

# Injected an IPython shell just before the setuptools import in Cython/Distutils/build_ext.py
In [1]: _build_ext_module

# Should return the build_ext module. Hmm, we can't get the build_ext module. Let's check the parents.
In [2]:  sys.modules.get('setuptools.command.build_ext')

# We can get the setuptools module
In [3]:  sys.modules.get('setuptools')
Out[3]: <module 'setuptools' from '/Users/lmwangi/Library/Caches/pypoetry/virtualenvs/wallet-api-ZqjMZXwH-py3.9/lib/python3.9/site-packages/setuptools/__init__.py'>


# We can get the setuptools.command module
In [5]:  sys.modules.get('setuptools.command')
Out[5]: <module 'setuptools.command' from '/Users/lmwangi/Library/Caches/pypoetry/virtualenvs/wallet-api-ZqjMZXwH-py3.9/lib/python3.9/site-packages/setuptools/command/__init__.py'>

# On the filesystem, the module does exist.
In [6]: !ls /Users/lmwangi/Library/Caches/pypoetry/virtualenvs/wallet-api-ZqjMZXwH-py3.9/lib/python3.9/site-packages/setuptools/command
 __init__.py   alias.py       bdist_rpm.py    build_ext.py   develop.py     easy_install.py   install.py	    install_lib.py	'launcher manifest.xml'   register.py   saveopts.py   setopt.py   upload.py
 __pycache__   bdist_egg.py   build_clib.py   build_py.py    dist_info.py   egg_info.py       install_egg_info.py   install_scripts.py	 py36compat.py		  rotate.py     sdist.py      test.py	  upload_docs.py


# Hmm, import does work
In [8]: import setuptools.command.build_ext

# Now a get works....
In [9]:  sys.modules.get('setuptools.command.build_ext')
Out[9]: <module 'setuptools.command.build_ext' from '/Users/lmwangi/Library/Caches/pypoetry/virtualenvs/wallet-api-ZqjMZXwH-py3.9/lib/python3.9/site-packages/setuptools/command/build_ext.py'>

Digging further, it looks like this is the [breaking commit](https://github.com/cython/cython/commit/c6f5c5ddcc021099febae7cb1194fedea01fd056).

< # Always inherit from the "build_ext" in distutils since setuptools already imports
< # it from Cython if available, and does the proper distutils fallback otherwise.
< # https://github.com/pypa/setuptools/blob/9f1822ee910df3df930a98ab99f66d18bb70659b/setuptools/command/build_ext.py#L16
<
< # setuptools imports Cython's "build_ext", so make sure we go first.
< _build_ext_module = sys.modules.get('setuptools.command.build_ext')
< if _build_ext_module is None:
<     import distutils.command.build_ext as _build_ext_module
<
< # setuptools remembers the original distutils "build_ext" as "_du_build_ext"
< _build_ext = getattr(_build_ext_module, '_du_build_ext', None)
< if _build_ext is None:
<     _build_ext = getattr(_build_ext_module, 'build_ext', None)
< if _build_ext is None:
---
> if 'setuptools' in sys.modules:
>     try:
>         from setuptools.command.build_ext import build_ext as _build_ext
>     except ImportError:
>         # We may be in the process of importing setuptools, which tries
>         # to import this.
>         from distutils.command.build_ext import build_ext as _build_ext

If we revert the import in Cython/Distutils/build_ext.py with the one in cython 0.x, the build now works

 ❯  head -17 /Users/lmwangi/Library/Caches/pypoetry/virtualenvs/poetry-pypi-example-wSzIWKA2-py3.9/lib/python3.9/site-packages/Cython/Distutils/build_ext.py 
import sys
import os


import sys

if 'setuptools' in sys.modules:
    try:
        from setuptools.command.build_ext import build_ext as _build_ext
    except ImportError:
        # We may be in the process of importing setuptools, which tries
        # to import this.
        from distutils.command.build_ext import build_ext as _build_ext
else:
    from distutils.command.build_ext import build_ext as _build_ext


 ❯ poetry run Cython -V 
Cython version 3.0.0a10

  ❯ poetry build  
Building poetry-pypi-example (0.0.1)
  - Building sdist
  - Built poetry-pypi-example-0.0.1.tar.gz
  - Building wheel
[1/1] Cythonizing poetry_pypi_example/main.py
  - Built poetry_pypi_example-0.0.1-cp39-cp39-macosx_12_0_x86_64.whl

@labeneator Hello! Did you figure out how to fix this error and use that script with latest version of Cython and Poetry?

@labeneator Hello! Did you figure out how to fix this error and use that script with latest version of Cython and Poetry?

With the small patch above, it worked. However, I had to abandon the cython route because pydantic <> cython does not work. Cython maintainers were unwilling to revert the patch as per: https://groups.google.com/g/cython-users/c/CQ2ieXACtzk/m/guDy3kFZBAAJ

@labeneator Thanks for your answer! I already fixed this tomorrow by editing build.py a bit:

...

def build(setup_kwargs: Dict[str, Any]) -> Dict[str, Any]:
    # Generate stub files for type hints
    generate_stub_files()

    # Collect and cythonize all files
    extension_modules = cythonize_helper(get_extension_modules())

    # Use Setuptools to collect files
    distribution = Distribution(
        {
            "ext_modules": extension_modules,
            "cmdclass": {
                "build_ext": cython_build_ext,
            },
        }
    )

    # Grab the build_ext command
    build_ext_cmd = distribution.get_command_obj("build_ext")
    build_ext_cmd.ensure_finalized()
    # Set the value to 1 for "inplace", with the goal to build extensions
    # in build directory, and then copy all files back to the source dir
    # (under the hood, "copy_extensions_to_source" will be called after
    # building the extensions). This is done so Poetry grabs the files
    # during the next step in its build.
    build_ext_cmd.inplace = 1
    build_ext_cmd.run()

    return setup_kwargs
    
...

if __name__ == '__main__':
    build({})

And here are my build settings in pyproject.toml:

[tool.poetry]
...
# Only include cythonized files in final build
include = ["mylib/**/*.so", "mylib/**/*.pyd"]  # ignored in VCS, so we need to be explicit
exclude = ["mylib/**/*.py"]
...
[tool.poetry.build]
generate-setup-file = false
script = "build.py"
...

Now I'm using the latest version of Poetry and the latest version of Cython (3.0.0a11).

And as a result, I get a protected package without source code, only with .pyd extensions, and I also added auto-generation of .pyi files so that hints and types remain when using the library.

I heard that pydantic is good, but simple typing and sometimes dataclasses are enough for me. The main thing is that the source code is protected.

And I also recommend not converting Path to the str(Path) string, but using the .as_posix method, because backslashes on Windows can cause problems. Because of this, the cythonization process did not start for me at all.

So you need to replace this

# Convert path to module name
module_path = str(module_path).replace("/", ".")

to this

# Convert path to module name
module_path = module_path.as_posix().replace("/", ".")

and in other places too