Building Python submodules from C++ extensions via cmake - python

I'm trying to incorporate a c++ extension as a submodule into an existing python library via cmake. Building the C++ extension works fine and importing it as a python module works, but not as the submodule of the header library.
I have the following directory structure:
frontend/
foo.py
bar.py
backend/
backend.cpp
The extension is bound to a python module via pybind:
PYBIND11_MODULE(backend, m)
{
m.doc() = "backend c++ implementation"; // optional module docstring
m.def("method", &method, "The method I want to call from python.");
}
In the CMakeLists.txt, the relevant line is:
pybind11_add_module(backend "frontend/backend/backend.cpp")
I've followed the instructions form here and here to write the setup.py script. I guess the most important lines look like this:
from setuptools import setup, Extension, find_packages
from setuptools.command.build_ext import build_ext
from setuptools.command.test import test as TestCommand
class CMakeExtension(Extension):
def __init__(self, name, sourcedir=".", sources=[]):
Extension.__init__(self, name, sources=[])
class CMakeBuild(build_ext):
def run(self):
build_directory = os.path.abspath(self.build_temp)
if not os.path.exists(self.build_temp):
os.makedirs(self.build_temp)
cmake_list_dir = os.path.abspath(os.path.dirname(__file__))
print("-" * 10, "Running CMake prepare", "-" * 40)
subprocess.check_call(
["cmake", cmake_list_dir], cwd=self.build_temp,
)
print("-" * 10, "Building extensions", "-" * 40)
cmake_cmd = ["cmake", "--build", "."] + self.build_args
subprocess.check_call(cmake_cmd, cwd=self.build_temp)
# Move from build temp to final position
for ext in self.extensions:
self.move_output(ext)
def move_output(self, ext):
build_temp = Path(self.build_temp).resolve()
dest_path = Path(self.get_ext_fullpath(ext.name)).resolve()
source_path = build_temp / self.get_ext_filename(ext.name)
dest_directory = dest_path.parents[0]
dest_directory.mkdir(parents=True, exist_ok=True)
self.copy_file(source_path, dest_path)
extensions = [CMakeExtension("backend")]
setup(
name="frontend",
packages=["frontend"],
ext_modules=extensions,
cmdclass=dict(build_ext=CMakeBuild),
)
But this does not make backend a submodule of frontend, but instead a module on its own. So this works:
from backend import method
But to avoid naming issues with other libraries, what I would like to have is this:
from frontend.backend import method
Changing the naming in the pybinding or in the extension call to extensions = [CMakeExtension("frontend.backend")] does unfortunately not resolve my problem, the setup does not find the backend.<platform>.so shared library then, because it looks for frontend/backend.<platform>.so, which does not exist. How could I resolve this issue?

I think I've resolved the issue with the following lines:
Change in the setup.py file:
ext_modules = [
Extension(
"frontend.backend", sources=["frontend/backend/backend.cpp"]
)
]
Change in the CMakeLists.txt file:
pybind11_add_module(backend "frontend/backend/backend.cpp")
set_target_properties( backend
PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/frontend"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/frontend"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/frontend"
)
The shared library object backend.platform.so must be located in the frontend directory. Neither the pybind module name nor the sourcefile .cpp should contain any "." in the names, because the get_ext_fullpath() method from build_ext will split by dots. Only the frontend directory containts an init.py file.

Related

How to include shared object files in wheel built from Poetry

I have a simple package that includes a C extension. I'm managing my dependencies and build process with Poetry.
When I run poetry build, the extension is compiled and included in the .tar.gz archive but not in the .whl and I don't understand why. Pip installing from the tar.gz archive works as expected but since the wheel is lacking the .so, pip installing the wheel results in an unusable package.
I've lifted the build machinery from here: https://github.com/python-poetry/poetry/issues/2740
pyproject.toml
[tool.poetry]
name = "python_ctypes"
version = "0.1.0"
description = ""
authors = ["Me"]
include = [
{path = "_extensions/*.so", format = "wheel"}
]
[tool.poetry.dependencies]
python = "^3.9"
numpy = "^1.22.1"
[tool.poetry.dev-dependencies]
[tool.poetry.build]
generate-setup-file = false
script = "build.py"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
build.py
"""Poetry build script for python_ctypes"""
import os
import shutil
from distutils.command.build_ext import build_ext
from distutils.core import Distribution
from distutils.core import Extension
from distutils.errors import CCompilerError
from distutils.errors import DistutilsExecError
from distutils.errors import DistutilsPlatformError
extensions = [
Extension("python_ctypes._extensions.arraysum", ["python_ctypes/_extensions/arraysum.c"]),
]
class ExtBuilder(build_ext):
# This class allows C extension building to fail.
built_extensions = []
def run(self):
try:
build_ext.run(self)
except (DistutilsPlatformError, FileNotFoundError):
print("Unable to build the C extensions")
def build_extension(self, ext):
try:
build_ext.build_extension(self, ext)
except (CCompilerError, DistutilsExecError, DistutilsPlatformError, ValueError):
print('Unable to build the "{}" C extension, '
"python_ctypes will use the pure python version of the extension.".format(ext.name))
def build(setup_kwargs):
"""
This function is mandatory in order to build the extensions.
"""
print(setup_kwargs)
distribution = Distribution({"name": "python_ctypes", "ext_modules": extensions})
distribution.package_dir = "python_ctypes"
cmd = ExtBuilder(distribution)
cmd.ensure_finalized()
cmd.run()
# Copy built extensions back to the project
for output in cmd.get_outputs():
relative_extension = os.path.relpath(output, cmd.build_lib)
if not os.path.exists(output):
continue
shutil.copyfile(output, relative_extension)
mode = os.stat(relative_extension).st_mode
mode |= (mode & 0o444) >> 2
os.chmod(relative_extension, mode)
return setup_kwargs
if __name__ == "__main__":
build({})

Pyinstaller - include programmatically imported modules

I have a package that sort of looks like this:
- package
-- module1.py
-- module2.py
-- __init__.py
In init.py I am programmatically scanning the package and importing the modules that are inside.
import importlib
import pkgutil
registry = {}
def creatable(cls):
if cls.handles_type() in registry:
raise KeyError("Duplicate string representations found for string: " + cls.handles_type())
registry[cls.handles_type()] = cls
return cls
def import_submodules(package, recursive=False):
""" Import all submodules of a module, recursively, including subpackages
"""
if isinstance(package, str):
package = importlib.import_module(package)
results = {}
for loader, name, is_pkg in pkgutil.walk_packages(package.__path__):
full_name = package.__name__ + '.' + name
results[full_name] = importlib.import_module(full_name)
if recursive and is_pkg:
results.update(import_submodules(full_name))
return results
import_submodules(__name__)
Inside the modules, there are classes annotated with the #creatable decorator that I define here. The idea is to have a registry dictionary with the key being the class name and the value being the class (so I can create instances using the string representation).
The issue is, the registry is empty when using Pyinstaller.
UPDATE:
The issue is that package.__path__ does not contain anything. It can be fixed by adding the .py files on the package path i.e.
datas=[
('package/*.py', 'package'),
]
But this doesn't look like a good solution - i.e. I'd be sending code to the end user.
This isn't an answer but is the only way I can show some code - this is my hook file for package mypkg, in the hooks folder, called hook-mypkg.py
import os
imports = []
for root, dirs, files in os.walk(os.path.join(os.getcwd(),"mypkg" )):
print(root)
for file in files:
if file.endswith( ".py") and not file.endswith( "__init__.py"):
print( " ",file)
imports.append("mypkg."+file[:-3])
print( "........." )
print( "hi=",imports )
hiddenimports = imports
This definitely works to include the .py files it finds in the bundle - they appear in globals()['__loader__'].toc in init.py as e.g. "mypkg.file1" for module mypkg\file1.py when sys.frozen is True.
Note after editing the hook you have to delete the dist and build folders then re-run pyinstaller

distutils wildcard entry for include_dir

I am setting up a package using distutils.
I need to allow access to a module that is built during the set-up process and is located in ./build/temp.linux-x86_64-3.6. I do this by including the
include_dirs=["./build/temp.linux-x86_64-3.6"]
when adding the extension to the distutils Configuration.
My question is there a way of setting this using a wildcard such as:
include_dirs=["./build/temp.linux*"]
as when I try this it fails, citing error:
Nonexistent include directory ‘build/temp.linux*’ [-Wmissing-include-dirs]
The reason I want this is that the build folder will be named differently depending on the system. Alternatively if anyone knows a way of figuring out what this temp build folder will be called that would also work.
The way I have got around this problem is as follows:
def return_major_minor_python():
import sys
return str(sys.version_info[0])+"."+str(sys.version_info[1])
def return_include_dir():
from distutils.util import get_platform
return get_platform()+'-'+return_major_minor_python()
Then when calling config.add_extension() using:
include_dirs=['build/temp.' + return_include_dir()]
So the whole process for adding a f90wrapped, f2py extension to a python package is:
def setup_fort_ext(args,parent_package='',top_path=''):
from numpy.distutils.misc_util import Configuration
from os.path import join
import sys
config = Configuration('',parent_package,top_path)
fort_src = [join('PackageName/','fortran_source.f90')]
config.add_library('_fortran_source', sources=fort_src,
extra_f90_compile_args = [ args["compile_args"]],
extra_link_args=[args["link_args"]])
sources = [join('PackageName','f90wrap_fortran_source.f90')]
config.add_extension(name='_fortran_source',
sources=sources,
extra_f90_compile_args = [ args["compile_args"]],
extra_link_args=[args["link_args"]],
libraries=['_tort'],
include_dirs=['build/temp.' + return_include_dir()])
return config
if __name__ == '__main__':
import sys
import subprocess
import os
install_numpy() #installs numpy
install_dependencies() #calls to pip to install any requirements
from numpy.distutils.core import setup
config = {'name':'PackageName',
'version':__version__,
'project_description':'Some Package description',
'description':'Some package Description',
'long_description': open('README.txt').read(),
'long_description_content_type':'text/markdown',
'author':'Your name here',
'author_email':'your email here',
'url':'link to git repo here',
'python_requires':'>=3.3',
'packages':['PackageName'],
'package_dir':{'PackageName':'PackageName'},
'package_data':{'PackageName':['*so*']},
'name': 'PackageName'
}
config_fort = setup_fort_ext(args,parent_package='PackageName',top_path='')
config2 = dict(config,**config_fort.todict())
setup(**config2)
where the source fortran_source.f90 is wrapped beforehand and the resulting wrapped source file (f90wrap_fortran_source.f90) is included as a library, and subsequently compiled by f2py.
args in the above is just a dict with the any linking or compile args you wish to pass through.

Can I create a static Cython library using distutils?

I'd like to build a static Cython library using distutils. I don't care about having it be a true Python extension module that can be import'ed. I just want to compile the code and put the objects in a static library. The code to create a dynamic library is very simple,
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
setup(
cmdclass = {'build_ext':build_ext},
ext_modules = [Extension("test",["test.pyx"])]
)
Is there a simple way to make it static instead?
Distutils is very limited and not set up for static builds. I would advise you to use something else to compile the static library part of your project.
If your use case is to call into Cython code from other C code, then you want to use the public or api declarations along with your cdef declared functions and variables in your Cython code. Cython will allow the so-declared objects to be called from external C code, and it will generate a .h file alongside the .c file for you.
Fyi, this works using numpy distutils, but obviously is nowhere near the simplicity or probably portability of the original code for a shared library,
from Cython.Compiler.Main import compile
from numpy.distutils.misc_util import Configuration
compile('test.pyx')
config = Configuration(...)
config.add_installed_library('test',
['test.c'],
'test',
{'include_dirs':[get_python_inc()]})
Assuming that you have a sources, include_dirs and build_dir in your setup.py, this is how you can build a static library
from distutils.ccompiler import new_compiler
from sysconfig import get_paths
import os
project_name = "slimey_project"
source = ['source1.c']
include_dirs = ['include']
build_dir = os.path.join(os.path.dirname(__file__), 'build')
class StaticLib(Command):
description = 'build static lib'
user_options = [] # do not remove, needs to be stubbed out!
python_info = get_paths()
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
# Create compiler with default options
c = new_compiler()
# Optionally add include directories etc.
for d in include_dirs:
c.add_include_dir(d)
c.add_include_dir(self.python_info['include'])
# Compile into .o files
objects = c.compile(sources)
# Create static or shared library
c.create_static_lib(objects, project_name, output_dir=build_dir)
Source: https://gist.github.com/udnaan/d549950a33fd82d13f9e6ba4aae82964

How can I get the version defined in setup.py (setuptools) in my package?

How could I get the version defined in setup.py from my package (for --version, or other purposes)?
Interrogate version string of already-installed distribution
To retrieve the version from inside your package at runtime (what your question appears to actually be asking), you can use:
import pkg_resources # part of setuptools
version = pkg_resources.require("MyProject")[0].version
Store version string for use during install
If you want to go the other way 'round (which appears to be what other answer authors here appear to have thought you were asking), put the version string in a separate file and read that file's contents in setup.py.
You could make a version.py in your package with a __version__ line, then read it from setup.py using execfile('mypackage/version.py'), so that it sets __version__ in the setup.py namespace.
Warning about race condition during install
By the way, DO NOT import your package from your setup.py as suggested in another answer here: it will seem to work for you (because you already have your package's dependencies installed), but it will wreak havoc upon new users of your package, as they will not be able to install your package without manually installing the dependencies first.
example study: mymodule
Imagine this configuration:
setup.py
mymodule/
/ __init__.py
/ version.py
/ myclasses.py
Then imagine some usual scenario where you have dependencies and setup.py looks like:
setup(...
install_requires=['dep1','dep2', ...]
...)
And an example __init__.py:
from mymodule.myclasses import *
from mymodule.version import __version__
And for example myclasses.py:
# these are not installed on your system.
# importing mymodule.myclasses would give ImportError
import dep1
import dep2
problem #1: importing mymodule during setup
If your setup.py imports mymodule then during setup you would most likely get an ImportError. This is a very common error when your package has dependencies. If your package does not have other dependencies than the builtins, you may be safe; however this isn't a good practice. The reason for that is that it is not future-proof; say tomorrow your code needs to consume some other dependency.
problem #2: where's my __version__ ?
If you hardcode __version__ in setup.py then it may not match the version that you would ship in your module. To be consistent, you would put it in one place and read it from the same place when you need it. Using import you may get the problem #1.
solution: à la setuptools
You would use a combination of open, exec and provide a dict for exec to add variables:
# setup.py
from setuptools import setup, find_packages
from distutils.util import convert_path
main_ns = {}
ver_path = convert_path('mymodule/version.py')
with open(ver_path) as ver_file:
exec(ver_file.read(), main_ns)
setup(...,
version=main_ns['__version__'],
...)
And in mymodule/version.py expose the version:
__version__ = 'some.semantic.version'
This way, the version is shipped with the module, and you do not have issues during setup trying to import a module that has missing dependencies (yet to be installed).
The best technique is to define __version__ in your product code, then import it into setup.py from there. This gives you a value you can read in your running module, and have only one place to define it.
The values in setup.py are not installed, and setup.py doesn't stick around after installation.
What I did (for example) in coverage.py:
# coverage/__init__.py
__version__ = "3.2"
# setup.py
from coverage import __version__
setup(
name = 'coverage',
version = __version__,
...
)
UPDATE (2017): coverage.py no longer imports itself to get the version. Importing your own code can make it uninstallable, because you product code will try to import dependencies, which aren't installed yet, because setup.py is what installs them.
Your question is a little vague, but I think what you are asking is how to specify it.
You need to define __version__ like so:
__version__ = '1.4.4'
And then you can confirm that setup.py knows about the version you just specified:
% ./setup.py --version
1.4.4
I wasn't happy with these answers... didn't want to require setuptools, nor make a whole separate module for a single variable, so I came up with these.
For when you are sure the main module is in pep8 style and will stay that way:
version = '0.30.unknown'
with file('mypkg/mymod.py') as f:
for line in f:
if line.startswith('__version__'):
_, _, version = line.replace("'", '').split()
break
If you'd like to be extra careful and use a real parser:
import ast
version = '0.30.unknown2'
with file('mypkg/mymod.py') as f:
for line in f:
if line.startswith('__version__'):
version = ast.parse(line).body[0].value.s
break
setup.py is somewhat of a throwaway module so not an issue if it is a bit ugly.
Update: funny enough I've moved away from this in recent years and started using a separate file in the package called meta.py. I put lots of meta data in there that I might want to change frequently. So, not just for one value.
With a structure like this:
setup.py
mymodule/
/ __init__.py
/ version.py
/ myclasses.py
where version.py contains:
__version__ = 'version_string'
You can do this in setup.py:
import sys
sys.path[0:0] = ['mymodule']
from version import __version__
This won't cause any problem with whatever dependencies you have in your mymodule/__init__.py
Create a file in your source tree, e.g. in yourbasedir/yourpackage/_version.py . Let that file contain only a single line of code, like this:
__version__ = "1.1.0-r4704"
Then in your setup.py, open that file and parse out the version number like this:
verstr = "unknown"
try:
verstrline = open('yourpackage/_version.py', "rt").read()
except EnvironmentError:
pass # Okay, there is no version file.
else:
VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]"
mo = re.search(VSRE, verstrline, re.M)
if mo:
verstr = mo.group(1)
else:
raise RuntimeError("unable to find version in yourpackage/_version.py")
Finally, in yourbasedir/yourpackage/__init__.py import _version like this:
__version__ = "unknown"
try:
from _version import __version__
except ImportError:
# We're running in a tree that doesn't have a _version.py, so we don't know what our version is.
pass
An example of code that does this is the "pyutil" package that I maintain. (See PyPI or google search -- stackoverflow is disallowing me from including a hyperlink to it in this answer.)
#pjeby is right that you shouldn't import your package from its own setup.py. That will work when you test it by creating a new Python interpreter and executing setup.py in it first thing: python setup.py, but there are cases when it won't work. That's because import youpackage doesn't mean to read the current working directory for a directory named "yourpackage", it means to look in the current sys.modules for a key "yourpackage" and then to do various things if it isn't there. So it always works when you do python setup.py because you have a fresh, empty sys.modules, but this doesn't work in general.
For example, what if py2exe is executing your setup.py as part of the process of packaging up an application? I've seen a case like this where py2exe would put the wrong version number on a package because the package was getting its version number from import myownthing in its setup.py, but a different version of that package had previously been imported during the py2exe run. Likewise, what if setuptools, easy_install, distribute, or distutils2 is trying to build your package as part of a process of installing a different package that depends on yours? Then whether your package is importable at the time that its setup.py is being evaluated, or whether there is already a version of your package that has been imported during this Python interpreter's life, or whether importing your package requires other packages to be installed first, or has side-effects, can change the results. I've had several struggles with trying to re-use Python packages which caused problems for tools like py2exe and setuptools because their setup.py imports the package itself in order to find its version number.
By the way, this technique plays nicely with tools to automatically create the yourpackage/_version.py file for you, for example by reading your revision control history and writing out a version number based on the most recent tag in revision control history. Here is a tool that does that for darcs: http://tahoe-lafs.org/trac/darcsver/browser/trunk/README.rst and here is a code snippet which does the same thing for git: http://github.com/warner/python-ecdsa/blob/0ed702a9d4057ecf33eea969b8cf280eaccd89a1/setup.py#L34
We wanted to put the meta information about our package pypackagery in __init__.py, but could not since it has third-party dependencies as PJ Eby already pointed out (see his answer and the warning regarding the race condition).
We solved it by creating a separate module pypackagery_meta.py that contains only the meta information:
"""Define meta information about pypackagery package."""
__title__ = 'pypackagery'
__description__ = ('Package a subset of a monorepo and '
'determine the dependent packages.')
__url__ = 'https://github.com/Parquery/pypackagery'
__version__ = '1.0.0'
__author__ = 'Marko Ristin'
__author_email__ = 'marko.ristin#gmail.com'
__license__ = 'MIT'
__copyright__ = 'Copyright 2018 Parquery AG'
then imported the meta information in packagery/__init__.py:
# ...
from pypackagery_meta import __title__, __description__, __url__, \
__version__, __author__, __author_email__, \
__license__, __copyright__
# ...
and finally used it in setup.py:
import pypackagery_meta
setup(
name=pypackagery_meta.__title__,
version=pypackagery_meta.__version__,
description=pypackagery_meta.__description__,
long_description=long_description,
url=pypackagery_meta.__url__,
author=pypackagery_meta.__author__,
author_email=pypackagery_meta.__author_email__,
# ...
py_modules=['packagery', 'pypackagery_meta'],
)
You must include pypackagery_meta into your package with py_modules setup argument. Otherwise, you can not import it upon installation since the packaged distribution would lack it.
This should also work, using regular expressions and depending on the metadata fields to have a format like this:
__fieldname__ = 'value'
Use the following at the beginning of your setup.py:
import re
main_py = open('yourmodule.py').read()
metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", main_py))
After that, you can use the metadata in your script like this:
print 'Author is:', metadata['author']
print 'Version is:', metadata['version']
Simple and straight, create a file called source/package_name/version.py with the following contents:
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
__version__ = "2.6.9"
Then, on your file source/package_name/__init__.py, you import the version for other people to use:
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
from .version import __version__
Now, you can put this on setup.py
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import re
import sys
try:
filepath = 'source/package_name/version.py'
version_file = open( filepath )
__version__ ,= re.findall( '__version__ = "(.*)"', version_file.read() )
except Exception as error:
__version__ = "0.0.1"
sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )
finally:
version_file.close()
Tested this with Python 2.7, 3.3, 3.4, 3.5, 3.6 and 3.7 on Linux, Windows and Mac OS. I used on my package which has Integration and Unit Tests for all theses platforms. You can see the results from .travis.yml and appveyor.yml here:
https://travis-ci.org/evandrocoan/debugtools/builds/527110800
https://ci.appveyor.com/project/evandrocoan/pythondebugtools/builds/24245446
An alternate version is using context manager:
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import re
import sys
try:
filepath = 'source/package_name/version.py'
with open( filepath ) as file:
__version__ ,= re.findall( '__version__ = "(.*)"', file.read() )
except Exception as error:
__version__ = "0.0.1"
sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )
You can also be using the codecs module to handle unicode errors both on Python 2.7 and 3.6
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import re
import sys
import codecs
try:
filepath = 'source/package_name/version.py'
with codecs.open( filepath, 'r', errors='ignore' ) as file:
__version__ ,= re.findall( '__version__ = "(.*)"', file.read() )
except Exception as error:
__version__ = "0.0.1"
sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )
If you are writing a Python module 100% in C/C++ using Python C Extensions, you can do the same thing, but using C/C++ instead of Python.
On this case, create the following setup.py:
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import re
import sys
import codecs
from setuptools import setup, Extension
try:
filepath = 'source/version.h'
with codecs.open( filepath, 'r', errors='ignore' ) as file:
__version__ ,= re.findall( '__version__ = "(.*)"', file.read() )
except Exception as error:
__version__ = "0.0.1"
sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )
setup(
name = 'package_name',
version = __version__,
package_data = {
'': [ '**.txt', '**.md', '**.py', '**.h', '**.hpp', '**.c', '**.cpp' ],
},
ext_modules = [
Extension(
name = 'package_name',
sources = [
'source/file.cpp',
],
include_dirs = ['source'],
)
],
)
Which reads the version from the file version.h:
const char* __version__ = "1.0.12";
But, do not forget to create the MANIFEST.in to include the version.h file:
include README.md
include LICENSE.txt
recursive-include source *.h
And it is integrated into the main application with:
#include <Python.h>
#include "version.h"
// create the module
PyMODINIT_FUNC PyInit_package_name(void)
{
PyObject* thismodule;
...
// https://docs.python.org/3/c-api/arg.html#c.Py_BuildValue
PyObject_SetAttrString( thismodule, "__version__", Py_BuildValue( "s", __version__ ) );
...
}
References:
python open file error
Define a global in a Python module from a C API
How to include package data with setuptools/distribute?
https://github.com/lark-parser/lark/blob/master/setup.py#L4
How to use setuptools packages and ext_modules with the same name?
Is it possible to include subdirectories using dist utils (setup.py) as part of package data?
To avoid importing a file (and thus executing its code) one could parse it and recover the version attribute from the syntax tree:
# assuming 'path' holds the path to the file
import ast
with open(path, 'rU') as file:
t = compile(file.read(), path, 'exec', ast.PyCF_ONLY_AST)
for node in (n for n in t.body if isinstance(n, ast.Assign)):
if len(node.targets) == 1:
name = node.targets[0]
if isinstance(name, ast.Name) and \
name.id in ('__version__', '__version_info__', 'VERSION'):
v = node.value
if isinstance(v, ast.Str):
version = v.s
break
if isinstance(v, ast.Tuple):
r = []
for e in v.elts:
if isinstance(e, ast.Str):
r.append(e.s)
elif isinstance(e, ast.Num):
r.append(str(e.n))
version = '.'.join(r)
break
This code tries to find the __version__ or VERSION assignment at the top level of the module return is string value. The right side can be either a string or a tuple.
There's a thousand ways to skin a cat -- here's mine:
# Copied from (and hacked):
# https://github.com/pypa/virtualenv/blob/develop/setup.py#L42
def get_version(filename):
import os
import re
here = os.path.dirname(os.path.abspath(__file__))
f = open(os.path.join(here, filename))
version_file = f.read()
f.close()
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
A lot of the other answers are outdated, I believe the standard way to get version information from an installed python 3.10 package is by using importlib.metadata as of PEP-0566
Official docs: https://docs.python.org/3.10/library/importlib.metadata.html
from importlib.metadata import version
VERSION_NUM = version("InstalledPackageName")
This is simple, clean, and no fuss.
This won't work if you are doing something weird in a script that runs during package installation, but if all you are doing is getting the version number for a version check to show the user in through a CLI --help command, about box, or anything else where your package is already installed and just needs the installed version number this seems like the best solution to me.
Cleaning up https://stackoverflow.com/a/12413800 from #gringo-suave:
from itertools import ifilter
from os import path
from ast import parse
with open(path.join('package_name', '__init__.py')) as f:
__version__ = parse(next(ifilter(lambda line: line.startswith('__version__'),
f))).body[0].value.s
Now this is gross and needs some refining (there may even be an uncovered member call in pkg_resources that I missed), but I simply do not see why this doesn't work, nor why no one has suggested it to date (Googling around has not turned this up)...note that this is Python 2.x, and would require requiring pkg_resources (sigh):
import pkg_resources
version_string = None
try:
if pkg_resources.working_set is not None:
disto_obj = pkg_resources.working_set.by_key.get('<my pkg name>', None)
# (I like adding ", None" to gets)
if disto_obj is not None:
version_string = disto_obj.version
except Exception:
# Do something
pass
deploy package to server and file naming convention for indices packages :
example for pip dynamic version conversion:
win:
test_pkg-1.0.0-cp36-cp36m-win_amd64.whl
test_pkg-1.0.0-py3.6-win-amd64.egg
mac:
test_pkg-1.0.0-py3.7-macosx-10.12-x86_64.egg
test_pkg-1.0.0-py3.7-macosx-10.12-x86_64.whl
linux:
test_pkg-1.0.0-cp36-cp36m-linux_x86_64.whl
from setuptools_scm import get_version
def _get_version():
dev_version = str(".".join(map(str, str(get_version()).split("+")[0]\
.split('.')[:-1])))
return dev_version
Find the sample setup.py calls the dynamic pip version matching from git commit
setup(
version=_get_version(),
name=NAME,
description=DESCRIPTION,
long_description=LONG_DESCRIPTION,
classifiers=CLASSIFIERS,
# add few more for wheel wheel package ...conversion
)
For what is worth, I wrote getversion to solve this issue for one of our projects' needs. It relies on a sequence of PEP-compliant strategies to return the version for a module, and adds some strategies for development mode (git scm).
Example:
from getversion import get_module_version
# Get the version of an imported module
from xml import dom
version, details = get_module_version(dom)
print(version)
Yields
3.7.3.final.0
Why was this version found ? You can understand it from the details:
> print(details)
Version '3.7.3.final.0' found for module 'xml.dom' by strategy 'get_builtin_module_version', after the following failed attempts:
- Attempts for module 'xml.dom':
- <get_module_version_attr>: module 'xml.dom' has no attribute '__version__'
- Attempts for module 'xml':
- <get_module_version_attr>: module 'xml' has no attribute '__version__'
- <get_version_using_pkgresources>: Invalid version number: None
- <get_builtin_module_version>: SUCCESS: 3.7.3.final.0
More can be found in the documentation.
I am using an environment variable as below
VERSION=0.0.0 python setup.py sdist bdist_wheel
In setup.py
import os
setup(
version=os.environ['VERSION'],
...
)
For consistency check with packer version, I am using below script.
PKG_VERSION=`python -c "import pkg; print(pkg.__version__)"`
if [ $PKG_VERSION == $VERSION ]; then
python setup.py sdist bdist_wheel
else
echo "Package version differs from set env variable"
fi
I created the regex pattern to find version number from setup.cfg ?:[\s]+|[\s])?[=](?:[\s]+|[\s])?(.*)
import re
with open("setup.cfg", "r") as _file:
data = _file.read()
print(re.findall(r"\nversion(?:[\s]+|[\s])?[=](?:[\s]+|[\s])?(.*)", data))
# -> ['1.1.0']
You can add this code to your __init__.py:
VERSION = (0, 3, 0)
def get_version():
"""Return the VERSION as a string.
For example, if `VERSION == (0, 10, 7)`, return '0.10.7'.
"""
return ".".join(map(str, VERSION))
__version__ = get_version()
And add this to the setup.py:
def get_version(version_tuple):
"""Return the version tuple as a string, e.g. for (0, 10, 7),
return '0.10.7'.
"""
return ".".join(map(str, version_tuple))
init = os.path.join(os.path.dirname(__file__), "your_library", "__init__.py")
version_line = list(filter(lambda line: line.startswith("VERSION"), open(init)))[0]
VERSION = get_version(eval(version_line.split("=")[-1]))
And finally, you can add the version=VERSION, line to the setup:
setup(
name="your_library",
version=VERSION,
)
I've solved this issue in the following way:
Created a version.py inside my module:
setup.py
mymodule/
/ __init__.py
/ version.py
/ myclasses.py
version.py
__version__ = '1.0.0'
setup.py
import sys
sys.path.insert(0, ("./mymodule"))
from version import __version__
I've avoided dependency this way. Of course, I'was inspired by many answers here.
Thank you!

Categories

Resources