Coder Social home page Coder Social logo

merak's Introduction

Merak

Merak is a package building toolkit.

This project is started as an attempt to implement a tool that builds a single Cython extension from a Python package, based on the discussion on StackOverflow - Collapse multiple submodules to one Cython extension. See the Idea section below.

More features and functionalities may be added in the future.

Install

To install the current release:

$ pip install merak

To upgrade Merak to the latest version, add --upgrade flag to the above command.

Usage

Currently, Merak only supports the cythonize command for building binary extension from a Python package. More features and functionalities may be added in the future.

To build a binary extension from a Python package:

$ merak cythonize PACKAGE_PATH OUTPUT_PATH

The package built will be placed at <OUTPUT_PATH>/<PACKAGE_NAME>. If -f is specified, any existing file / directory at this path will be overwritten.

usage: merak cythonize [-h] [-v] [-k] [-s SEP] [-f] path output

positional arguments:
  path               Python package path
  output             Output directory

optional arguments:
  -h, --help         show this help message and exit
  -v, --verbose      Log verbosity level. Default -> WARNING, -v -> INFO, -vv
                     or above -> DEBUG.
  -k, --color        Display logging messages in colors.
  -s SEP, --sep SEP  Module layer separator, must be Python identifier.
                     Defaults to '_'
  -f, --force        Force overwrite if target path exists
  --py-cmd PY_CMD    Python interpreter to be used for building Cython
                     package. Defaults to value of environment variable
                     "PYTHON_CMD", or "python" if "PYTHON_CMD" not specified.

Example

An example package foo is included in the examples/ directory. It consists of one subpackage bar with a module baz containing a function do() in it.

To build the foo package, run the following command in the project root:

$ merak cythonize examples/foo foo-build

The foo binary package can then be found at foo-build/foo. Change directory to foo-build and use an interactive Python session to try it out:

>>> from foo.bar import baz
__main__:1: DeprecationWarning: Deprecated since Python 3.4. Use importlib.util.find_spec() instead.
>>> baz.do()
Running: foo.bar.baz.do()

The deprecation warning seems to originate from the import logic in the compiled __init__ extension by Cython. It should cause no execution problems at all.

The binary package can be built into a Python distribution via setuptools by simply adding a setup.py in the output directory that includes the cython extension as package data. For this example, add setup.py to foo-build/ with the following content:

import setuptools

setuptools.setup(
    name="foo",
    version="0.1.0",
    packages=["foo"],
    include_package_data=True,
    package_data={"foo": ["*"]},
)

and run

$ python setup.py bdist_wheel

The distribution can be found at foo-build/dist/.

Idea

Based on this answer, it appears that it is possible to build a single Cython extension with multiple modules included in it.

However, it does NOT work with multi-level packages. Cython builds a C source file for each module with an initializer named PyInit_xxx, which depends on the base name of the module. As the function is defined in the global scope, a name collision would happen if the same base name is used for different modules. For instance, the following package would have a name collision for __init__.py and base.py:

foo/
  __init__.py
  bar/
    __init__.py
    base.py
  baz/
    __init__.py
    base.py

Here, we solve the problem in two steps:

  1. Module Flattening: We move all modules to the base layer, with name constructed from their original relative path: path.replace(path_separator, sep), where sep is a legal Python identifier. For example, foo/bar/base.py -> foo/bar_sep_base.py if sep="_sep_".
  2. Import Redirection: We inject a finder inside the main __init__.py that redirects dotted-paths to their flattened counterparts. Using the above example, the finder redirects the import foo.bar.base to foo.bar_sep_base.

The injected finder is based on this answer with some modifications. See the template for implementation detail.

The result would contain a single __init__ extension inside the package folder. The package folder is still required for the builtin importer to load it as a package, rather than a module. The above example would result in a foo/ folder with a single __init__ Cython extension in it.

Resources

License

Apache License 2.0

merak's People

Contributors

dave-msk avatar yao1993 avatar its-monotype avatar

Stargazers

 avatar  avatar 凡者 avatar Fantix King avatar Damien avatar  avatar Alexandr Turilin avatar Chris Fedun avatar  avatar  avatar  avatar Qiulin Zhang  avatar Alex Zanegin avatar Yoshiki Sakamoto avatar Archie Zhang avatar  avatar Daniel Hrisca avatar Max Chang avatar vicalloy avatar  avatar Yeong-Her Wang avatar  avatar Pritam Ghanghas avatar  avatar  avatar  avatar Thananop Kobchaisawat avatar ohmycloud avatar Keaun avatar  avatar Angus Leung avatar Norman Lam avatar

Watchers

James Cloos avatar  avatar  avatar Norman Lam avatar

merak's Issues

Cannot load the compiled "lib\\__init__.pyd" using "import <parentpackage>.lib"

Hello Dave!

Im a spanish developer who is using your tool to build a python library and it really works really well, congratulations!
I have just one issue with it and i think it is related to the way the injected finder works.

As stated already in the title, i want to perform a loading of the library from another root package but somehow, it doesnt work. Check this out:

import lib --> works Ok!

import myparentpackage.lib --> doesn work and fails to load the library.

My library is fairly complex so maybe it has to do with the way i set the environment for compilation... But it looks more like an issue regarding the Importer not being able to process imports that start with anything else than the "lib\init.pyd".

My question/request is that, is there a way with the current tool version to do what i want?

Thanks in advance.

Module Flattening may break relative imports

Issue Description:

My project have such structure.

foo/
  |- bar/
  |  |- __init__.py
  |  |- core.py
  |- __init__.py
  |- constant.py

Inside foo.bar.core, I have a relative import to the foo.constant as below.

from ..constant import some_thing

It seems like the module flatten will not modify the relative imports to the correct level which cause

ValueError: attempted relative import beyond top-level package

when trying to import foo.bar.core using complied library.

Using absolute imports solves the problem with no issue.

from foo.constant import some_thing

I know it is not a good design pattern, and it will be good to support it.

Possible remedy:

  • Always transform the relative imports to absolute imports before flatten

merak.exe using absolute install path of python.exe under the hood

First of all, thanks for this amazing tool! It has spared us weeks of development and "reinventing the wheel".

We are incorporating part of our multi python versions compiling environment in a custom repository where merak is included.

Nevertheless, we are facing a bit tricky and annoying inconvenient: the "merak.exe" installed in ".\PythonXY\Scripts" folder is configured apparently during pip install with the absolute path of the pyhon.exe we are using. This is a big problem for us because it breaks portability and the fact that we can clone the repo in different root folders than the one used by the dev that installed merak for a custom specific python version.

We dont know how easy or tricky to change this is. We just thought it would be a "nice to have" and report it in order to get some feedback.

Use Case:
(1) python.exe pip install merak
(2) change the root directory where the PythonXY folder resides
(3) notice how the "merak.exe" command fails because it looks for the python.exe in the first installation absolute path.

Thanks in advance,
JC

Can't be resolved relative to the current directory.

I'm trying to use your too, but it seems like got an issue with the way I run it.

I have a package that contains other packages and modules. The formate of my project is like following

--projectName
---package_name
------subpakage1
---------module2
------subpakage2
---------module2
------subpakage3
---------module2
-----module1

So I run the following command

merak cythonize ./package_name built_backage

but I get the following error

ValueError: C:\Users\ABDELS~1\AppData\Local\Temp\tmpic3t6kta\Applications\subpakage1_init_.py can't be resolved relative to the current directory.
Either run absolufy-imports from the project root, or pass
--application-directories

I'm not sure how to fix that. I hope you can help.

Note: I'm using Windows with python 3.8

Thanks a lot

Execution fail on Linux system due to `python` command not found

Problem
On some Linux Distribution (Debian), the name python3 is used for Python 3. python is either referring to Python 2, or not installed at all.

I cannot run the library at all until I manually patch python to python3, then everything are working as expected.

The related location is this line.

Potential Solution
A trivial way is to check if python3 exist, if yes then use python3 else use python.
I am not sure if sys.executable will be able to help finding the correct Python interpreter location, it may able to get rid of command name dependency.

module 'foo' has no attribute 'baz'

image

use cmd merak cythonize examples/foo foo-build to generate .so file, but the .so file can't run when i import it:

image

the .so file name is not normal.What details have I lost?

image

Using build error

Hi,I using merak build example. but ...

image

Could you please help me?

FileNotFoundError when building on Windows

When building the exmaple on Windows, FileNotFoundError occured.

(pybinary) ➜  merak git:(master) merak cythonize examples/foo foo-build
Traceback (most recent call last):
  File "c:\anaconda3\envs\pybinary\lib\shutil.py", line 791, in move
    os.rename(src, real_dst)
FileNotFoundError: [WinError 3] 系统找不到指定的路径。: 'C:\\Users\\tm42h7\\AppData\\Local\\Temp\\tmprmcrvgn4\\foo\\bar\\baz.py' -> 'C:\\Users\\tm42h7\\AppData\\Local\\Temp\\tmprmcrvgn4\\foo\\bar\\___bar\\baz.py'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "c:\anaconda3\envs\pybinary\lib\runpy.py", line 194, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "c:\anaconda3\envs\pybinary\lib\runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "C:\Anaconda3\envs\pybinary\Scripts\merak.exe\__main__.py", line 7, in <module>
  File "c:\anaconda3\envs\pybinary\lib\site-packages\merak\main.py", line 34, in main
    app.run()
  File "c:\anaconda3\envs\pybinary\lib\site-packages\cement\core\foundation.py", line 916, in run
    return_val = self.controller._dispatch()
  File "c:\anaconda3\envs\pybinary\lib\site-packages\cement\ext\ext_argparse.py", line 808, in _dispatch
    return func()
  File "c:\anaconda3\envs\pybinary\lib\site-packages\merak\commands\cythonize.py", line 57, in __call__
    success = cybuild.build_package_cython_extension(
  File "c:\anaconda3\envs\pybinary\lib\site-packages\merak\core\cybuild.py", line 59, in build_package_cython_extension
    mods, sub_pkgs = _restructure_package(tmp_proot, sep=sep)
  File "c:\anaconda3\envs\pybinary\lib\site-packages\merak\core\cybuild.py", line 179, in _restructure_package
    project.do(rename_cs)
  File "c:\anaconda3\envs\pybinary\lib\site-packages\rope\base\project.py", line 113, in do
    self.history.do(changes, task_handle=task_handle)
  File "c:\anaconda3\envs\pybinary\lib\site-packages\rope\base\history.py", line 36, in do
    changes.do(change.create_job_set(task_handle, changes))
  File "c:\anaconda3\envs\pybinary\lib\site-packages\rope\base\change.py", line 65, in do
    change.do(job_set)
  File "c:\anaconda3\envs\pybinary\lib\site-packages\rope\base\change.py", line 126, in call
    function(self)
  File "c:\anaconda3\envs\pybinary\lib\site-packages\rope\base\change.py", line 201, in do
    self._operations.move(self.resource, self.new_resource)
  File "c:\anaconda3\envs\pybinary\lib\site-packages\rope\base\change.py", line 339, in move
    fscommands.move(resource.real_path, new_resource.real_path)
  File "c:\anaconda3\envs\pybinary\lib\site-packages\rope\base\fscommands.py", line 45, in move
    shutil.move(path, new_location)
  File "c:\anaconda3\envs\pybinary\lib\shutil.py", line 811, in move
    copy_function(src, real_dst)
  File "c:\anaconda3\envs\pybinary\lib\shutil.py", line 435, in copy2
    copyfile(src, dst, follow_symlinks=follow_symlinks)
  File "c:\anaconda3\envs\pybinary\lib\shutil.py", line 264, in copyfile
    with open(src, 'rb') as fsrc, open(dst, 'wb') as fdst:
FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\tm42h7\\AppData\\Local\\Temp\\tmprmcrvgn4\\foo\\bar\\___bar\\baz.py'

My environment is:

(pybinary) ➜  merak git:(master) conda list
# packages in environment at C:\Anaconda3\envs\pybinary:
#
# Name                    Version                   Build  Channel
ca-certificates           2021.7.5             haa95532_1
cement                    3.0.4                    pypi_0    pypi
certifi                   2021.5.30        py38haa95532_0
colorama                  0.4.4                    pypi_0    pypi
colorlog                  4.8.0                    pypi_0    pypi
cython                    0.29.24                  pypi_0    pypi
merak                     0.2.3                    pypi_0    pypi
openssl                   1.1.1k               h2bbff1b_0
pip                       21.1.3           py38haa95532_0
python                    3.8.10               hdbf39b2_7
rope                      0.19.0                   pypi_0    pypi
setuptools                51.3.3                   pypi_0    pypi
sqlite                    3.36.0               h2bbff1b_0
vc                        14.2                 h21ff451_1
vs2015_runtime            14.27.29016          h5e58377_2
wheel                     0.36.2             pyhd3eb1b0_0
wincertstore              0.2                      py38_0

build failure when module `if __name__ == "__main__"`

The __main__ guarded code was calling a function defined above in the module, and for some reason this was leading to the MSVC compiler exiting with code -2 (using the -v flag did not show additional information)

Don‘t support pyx files

Thank you for the wonderful tool, it helps a lot.
Currently, I have some package mixed with .py and .pyx codes, can "merak" support this situation?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.