With a simple setup.py file:
from setuptools import setup
setup(
name='foo',
version='1.2.3',
)
I can do
$> python setup.py --version
1.2.3
without installing the package.
Is there similar functionality for the equivalent pyproject.toml file:
[project]
name = "foo"
version = "1.2.3"
With Python 3.11+, something like this should work:
python3.11 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])"
This parses the TOML file directly, and assumes that version is not dynamic.
In some cases, version is declared dynamic in pyproject.toml, so it can not be parsed directly from this file and a solution (the only one?) is to actually build the project, or at least its metadata.
For this purpose, we can use the build.util.project_wheel_metadata() function from the build project, for example with a small script like this:
#!/usr/bin/env python
import argparse
import pathlib
import build.util
def _main():
args_parser = argparse.ArgumentParser()
args_parser.add_argument('path')
args = args_parser.parse_args()
path_name = getattr(args, 'path')
path = pathlib.Path(path_name)
#
metadata = build.util.project_wheel_metadata(path)
version = metadata.get('Version')
print(version)
if __name__ == '__main__':
_main()
Or as a one-liner:
python -c "import build.util; print(build.util.project_wheel_metadata('.').get('Version'))"
Related
In order to make pip or jupyter available from command-line on Windows no matter the current working directory with just pip ... or jupyter ..., Python on Windows seems to use this method:
put C:\Python37\Scripts in the PATH
create a small 100KB .exe file C:\Python37\Scripts\pip.exe or jupyter.exe that probably does not much more than calling a Python script with the interpreter (I guess?)
Then doing pip ... or jupyter ... in command-line works, no matter the current directory.
Question: how can I create a similar 100KB mycommand.exe that I could put in a directory which is in the PATH, so that I could do mycommand from anywhere in command-line?
Note: I'm not speaking about pyinstaller, cxfreeze, etc. (that I already know and have used before); I don't think these C:\Python37\Scripts\ exe files use this.
Assuming you are using setuptools to package this application, you need to create a console_scripts entry point as documented here:
https://packaging.python.org/guides/distributing-packages-using-setuptools/#console-scripts
For a single top-level module it could look like the following:
setup.py
#!/usr/bin/env python3
import setuptools
setuptools.setup(
py_modules=['my_top_level_module'],
entry_points={
'console_scripts': [
'mycommand = my_top_level_module:my_main_function',
],
},
# ...
name='MyProject',
version='1.0.0',
)
my_top_level_module.py
#!/usr/bin/env python3
def my_main_function():
print("Hi!")
if __name__ == '__main__':
my_main_function()
And then install it with a command such as:
path/to/pythonX.Y -m pip install path/to/MyProject
I am making a Python package that has a C++-extension module and someone else's shared library that it requires. I want everything installable via pip. My current setup.py file works when I use pip install -e . but when I don't use develop mode (e.i. omit the -e) I get "cannot open shared object file" when importing the module in Python. I believe the reason is that setuptools doesn't consider the shared library to be part of my package, so the relative link to the library is broken during installation when files are copied to the install directory.
Here is what my setup.py file looks like:
from setuptools import setup, Extension, Command
import setuptools.command.develop
import setuptools.command.build_ext
import setuptools.command.install
import distutils.command.build
import subprocess
import sys
import os
# This function downloads and builds the shared-library
def run_clib_install_script():
build_clib_cmd = ['bash', 'clib_install.sh']
if subprocess.call(build_clib_cmd) != 0:
sys.exit("Failed to build C++ dependencies")
# I make a new command that will build the shared-library
class build_clib(Command):
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
run_clib_install_script()
# I subclass install so that it will call my new command
class install(setuptools.command.install.install):
def run(self):
self.run_command('build_clib')
setuptools.command.install.install.run(self)
# I do the same for build...
class build(distutils.command.build.build):
sub_commands = [
('build_clib', lambda self: True),
] + distutils.command.build.build.sub_commands
# ...and the same for develop
class develop(setuptools.command.develop.develop):
def run(self):
self.run_command('build_clib')
setuptools.command.develop.develop.run(self)
# These are my includes...
# note that /clib/include only exists after calling clib_install.sh
cwd = os.path.dirname(os.path.abspath(__file__))
include_dirs = [
cwd,
cwd + '/clib/include',
cwd + '/common',
]
# These are my arguments for the compiler to my shared-library
lib_path = os.path.join(cwd, "clib", "lib")
library_dirs = [lib_path]
link_args = [os.path.join(lib_path, "libclib.so")]
# My extension module gets these arguments so it can link to clib
mygen_module = Extension('mygen',
language="c++14",
sources=["common/mygen.cpp"],
libraries=['clib'],
extra_compile_args=['-std=c++14'],
include_dirs=include_dirs,
library_dirs=library_dirs,
extra_link_args=link_args
+ ['-Wl,-rpath,$ORIGIN/../clib/lib'])
# I use cmdclass to override the default setuptool commands
setup(name='mypack',
cmdclass = {'install': install,
'build_clib': build_clib, 'build': build,
'develop': develop},
packages=['mypack'],
ext_package='mypack',
ext_modules=[mygen_module],
# package_dir={'mypack': '.'},
# package_data={'mypack': ['docs/*md']},
include_package_data=True)
I subclass some of the setuptools commands in order to build the shared-library before it compiles the extension. clib_install.sh is a bash script that locally downloads and builds the shared library in /clib, creating the headers (in /clib/include) and .so file (in /clib/lib). To solve problems with linking to shared-library dependencies I used $ORIGIN/../clib/lib as a link argument so that the absolute path to clib isn't needed.
Unfortunately, the /clib directory doesn't get copied to the install location. I tried tinkering with package_data but it didn't copy my directory over. In fact, I don't even know what pip/setuptools does with /clib after the script is called, I guess it is made in some temporary build directory and gets deleted after. I am not sure how to get /clib to where it needs to be after it is made.
package_data={
'mypack': [
'clib/include/*.h',
'clib/lib/*.so',
'docs/*md',
]
},
I've tried to implement Compass compiling during setuptools' build, but the following code runs compilation during explicit build command and doesn't runs during install.
#!/usr/bin/env python
import os
import setuptools
from distutils.command.build import build
SETUP_DIR = os.path.dirname(os.path.abspath(__file__))
class BuildCSS(setuptools.Command):
description = 'build CSS from SCSS'
user_options = []
def initialize_options(self):
pass
def run(self):
os.chdir(os.path.join(SETUP_DIR, 'django_project_dir', 'compass_project_dir'))
import platform
if 'Windows' == platform.system():
command = 'compass.bat compile'
else:
command = 'compass compile'
import subprocess
try:
subprocess.check_call(command.split())
except (subprocess.CalledProcessError, OSError):
print 'ERROR: problems with compiling Sass. Is Compass installed?'
raise SystemExit
os.chdir(SETUP_DIR)
def finalize_options(self):
pass
class Build(build):
sub_commands = build.sub_commands + [('build_css', None)]
setuptools.setup(
# Custom attrs here.
cmdclass={
'build': Build,
'build_css': BuildCSS,
},
)
Any custom instructions at Build.run (e.g. some printing) doesn't apply during install too, but dist instance contains in commands attribute only my build command implementation instances. Incredible! But I think the trouble is in complex relations between setuptools and distutils. Does anybody knows how to make custom building run during install on Python 2.7?
Update: Found that install definitely doesn't calls build command, but it calls bdist_egg which runs build_ext. Seems like I should implement "Compass" build extension.
Unfortunatelly, I haven't found the answer. Seems like the ability to run post-install scripts correctly there's only at Distutils 2. Now you can use this work-around:
Update: Because of setuptools' stack checks, we should override install.do_egg_install, not run method:
from setuptools.command.install import install
class Install(install):
def do_egg_install(self):
self.run_command('build_css')
install.do_egg_install(self)
Update2: easy_install runs exactly bdist_egg command which is used by install too, so the most correct way (espetially if you want to make easy_install work) is to override bdist_egg command. Whole code:
#!/usr/bin/env python
import setuptools
from distutils.command.build import build as _build
from setuptools.command.bdist_egg import bdist_egg as _bdist_egg
class bdist_egg(_bdist_egg):
def run(self):
self.run_command('build_css')
_bdist_egg.run(self)
class build_css(setuptools.Command):
description = 'build CSS from SCSS'
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
pass # Here goes CSS compilation.
class build(_build):
sub_commands = _build.sub_commands + [('build_css', None)]
setuptools.setup(
# Here your setup args.
cmdclass={
'bdist_egg': bdist_egg,
'build': build,
'build_css': build_css,
},
)
You may see how I've used this here.
I'm working on Windows and developing an application that will be distributed to end users.
I have one setupttools distribution that contains a nandful of python packages. This distribution declares some console_scripts entrypoints that point to various functions in my code.
One of these entrypoints is destined to be an updater application. I currently am launching it as a child process using subprocess and specifying the full path to a python script.
What i'd like to do instead is use the generated setuptools entrypoint stub executable as the subprocess to launch.
I can get the entrypoint to my subapplication via something like:
import pkg_resources
updatefunc = pkg_resources.load_entry_point('iti_reporter', 'console_scripts', 'my_update_ui')
which gives me the python function for it. Is there any way to go from this function to the generated setuptools script wrapper that resides in my venv?
Thanks for the assistance.
Figured it out by just doing what the script would have done anyway:
def _get_entry_script(dist, entrytype, entryname):
import pkg_resources
mainfunc = pkg_resources.load_entry_point(dist, entrytype, entryname)
entry_load_script = """import sys; from pkg_resources import load_entry_point;
sys.exit(
load_entry_point('%s', '%s', '%s')()
)"""
script = entry_load_script%(dist, entrytype, entryname)
script = ''.join(x.strip() for x in script.split('\n') if x.strip())
return script
def _get_updater_script_arg_dev():
return _get_entry_script('mydist', 'console_scripts', 'mypkg_update_ui')
def launch_updater():
cmd = (os.path.abspath(sys.executable), '-c', _get_updater_script_arg_dev(), '--arg1', '--arg2')
return subprocess.Popen(cmd)
I need to install a python module in the site packages that also will be used as a command line application. Suppose I have a module like:
app.py
def main():
print 'Dummy message'
if __name__ == '__main__':
main()
setup.py
import distutils
try:
from setuptools import setup
except ImportError:
from distutils.core import setup
if __name__ == '__main__':
setup(name = 'dummy',
version = '1.0',
packages = ['dummy'],
)
Creating the dist by:
setup.py sdist
Install:
setup.py install
And now I would like to use it as a command line application by opening the command window and typing just: dummy
Is it possible to create such application under windows without to carry out registering system pat variables and so on ...
You can use the options in setup.py to declare command line scripts. Please refer to this article. On Windows, the script will be created in "C:\Python26\Scripts" (if you didn't change the path) - lots of tools store their scripts there (e.g. "easy_install", "hg", ...).
Put the following in dummy.cmd:
python.exe -m dummy
Or is it dummy.app...
Oh well, it's one of those.