Skip to content

Commit cc13f80

Browse files
pipcl.py: Added asserts to avoid obscure build/runtime errors.
* Assert that file pyproject.toml exists if/when fn_build() is called; otherwise we can get obscure build errors. * Assert that sdist's contain file pyproject.toml, as require by spec. * When an extension shared library is returned from a pipcl.Package's fn_build(), and the extension was built by pipcl.py, assert that the extension was created with the same py_limited_api` arg as the package. This avoids obscure runtime errors. Also: * Improved docs for metadata args. * Added log() fn, same as log0(). * Added macos_add_brew_path(), for adding a brew package's binaries to $PATH.
1 parent cd79b59 commit cc13f80

File tree

1 file changed

+109
-15
lines changed

1 file changed

+109
-15
lines changed

pipcl.py

Lines changed: 109 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ class Package:
6969
by legacy distutils/setuptools and described in:
7070
https://pip.pypa.io/en/stable/reference/build-system/setup-py/
7171
72+
The file pyproject.toml must exist; this is checked if/when fn_build() is
73+
called.
74+
7275
Here is a `doctest` example of using pipcl to create a SWIG extension
7376
module. Requires `swig`.
7477
@@ -335,63 +338,86 @@ def __init__(self,
335338
wheel_compresslevel = None,
336339
):
337340
'''
338-
The initial args before `root` define the package
339-
metadata and closely follow the definitions in:
341+
The initial args before `entry_points` define the
342+
package metadata and closely follow the definitions in:
340343
https://packaging.python.org/specifications/core-metadata/
341344
342345
Args:
343346
344347
name:
348+
Used for metadata `Name`.
345349
A string, the name of the Python package.
346350
version:
351+
Used for metadata `Version`.
347352
A string, the version of the Python package. Also see PEP-440
348353
`Version Identification and Dependency Specification`.
349354
platform:
355+
Used for metadata `Platform`.
350356
A string or list of strings.
351357
supported_platform:
358+
Used for metadata `Supported-Platform`.
352359
A string or list of strings.
353360
summary:
361+
Used for metadata `Summary`.
354362
A string, short description of the package.
355363
description:
364+
Used for metadata `Description`.
356365
A string. If contains newlines, a detailed description of the
357366
package. Otherwise the path of a file containing the detailed
358367
description of the package.
359368
description_content_type:
369+
Used for metadata `Description-Content-Type`.
360370
A string describing markup of `description` arg. For example
361371
`text/markdown; variant=GFM`.
362372
keywords:
373+
Used for metadata `Keywords`.
363374
A string containing comma-separated keywords.
364375
home_page:
376+
Used for metadata `Home-page`.
365377
URL of home page.
366378
download_url:
379+
Used for metadata `Download-URL`.
367380
Where this version can be downloaded from.
368381
author:
382+
Used for metadata `Author`.
369383
Author.
370384
author_email:
385+
Used for metadata `Author-email`.
371386
Author email.
372387
maintainer:
388+
Used for metadata `Maintainer`.
373389
Maintainer.
374390
maintainer_email:
391+
Used for metadata `Maintainer-email`.
375392
Maintainer email.
376393
license:
394+
Used for metadata `License`.
377395
A string containing the license text. Written into metadata
378396
file `COPYING`. Is also written into metadata itself if not
379397
multi-line.
380398
classifier:
399+
Used for metadata `Classifier`.
381400
A string or list of strings. Also see:
382401
383402
* https://pypi.org/pypi?%3Aaction=list_classifiers
384403
* https://pypi.org/classifiers/
385404
386405
requires_dist:
387-
A string or list of strings. None items are ignored. Also see PEP-508.
406+
Used for metadata `Requires-Dist`.
407+
A string or list of strings, Python packages required
408+
at runtime. None items are ignored.
388409
requires_python:
410+
Used for metadata `Requires-Python`.
389411
A string or list of strings.
390412
requires_external:
413+
Used for metadata `Requires-External`.
391414
A string or list of strings.
392415
project_url:
393-
A string or list of strings, each of the form: `{name}, {url}`.
416+
Used for metadata `Project-URL`.
417+
A string or list of strings, each of the form: `{name},
418+
{url}`.
394419
provides_extra:
420+
Used for metadata `Provides-Extra`.
395421
A string or list of strings.
396422
397423
entry_points:
@@ -456,6 +482,11 @@ def __init__(self,
456482
default being `sysconfig.get_path('platlib')` e.g.
457483
`myvenv/lib/python3.9/site-packages/`.
458484
485+
When calling this function, we assert that the file
486+
pyproject.toml exists in the current directory. (We do this
487+
here rather than in pipcl.Package's constructor, as otherwise
488+
importing setup.py from non-package-related code could fail.)
489+
459490
fn_clean:
460491
A function taking a single arg `all_` that cleans generated
461492
files. `all_` is true iff `--all` is in argv.
@@ -474,8 +505,7 @@ def __init__(self,
474505
It can be convenient to use `pipcl.git_items()`.
475506
476507
The specification for sdists requires that the list contains
477-
`pyproject.toml`; we enforce this with a diagnostic rather than
478-
raising an exception, to allow legacy command-line usage.
508+
`pyproject.toml`; we enforce this with a Python assert.
479509
480510
tag_python:
481511
First element of wheel tag defined in PEP-425. If None we use
@@ -822,12 +852,11 @@ def add_string(text, name):
822852
assert 0, f'Path is inside sdist_directory={sdist_directory}: {from_!r}'
823853
assert os.path.exists(from_), f'Path does not exist: {from_!r}'
824854
assert os.path.isfile(from_), f'Path is not a file: {from_!r}'
825-
if to_rel == 'pyproject.toml':
826-
found_pyproject_toml = True
827855
add(from_, to_rel)
856+
if to_rel == 'pyproject.toml':
857+
found_pyproject_toml = True
828858

829-
if not found_pyproject_toml:
830-
log0(f'Warning: no pyproject.toml specified.')
859+
assert found_pyproject_toml, f'Cannot create sdist because file not specified: pyproject.toml'
831860

832861
# Always add a PKG-INFO file.
833862
add_string(self._metainfo(), 'PKG-INFO')
@@ -978,13 +1007,38 @@ def _entry_points_text(self):
9781007

9791008
def _call_fn_build( self, config_settings=None):
9801009
assert self.fn_build
1010+
assert os.path.isfile('pyproject.toml'), (
1011+
'Cannot create package because file does not exist: pyproject.toml'
1012+
)
9811013
log2(f'calling self.fn_build={self.fn_build}')
9821014
if inspect.signature(self.fn_build).parameters:
9831015
ret = self.fn_build(config_settings)
9841016
else:
9851017
ret = self.fn_build()
9861018
assert isinstance( ret, (list, tuple)), \
9871019
f'Expected list/tuple from {self.fn_build} but got: {ret!r}'
1020+
1021+
# Check that any extensions that we have built, have same
1022+
# py_limited_api value. If package is marked with py_limited_api=True
1023+
# then non-py_limited_api extensions seem to fail at runtime on
1024+
# Windows.
1025+
#
1026+
# (We could possibly allow package py_limited_api=False and extensions
1027+
# py_limited_api=True, but haven't tested this, and it seems simpler to
1028+
# be strict.)
1029+
for item in ret:
1030+
from_, (to_abs, to_rel) = self._fromto(item)
1031+
from_abs = os.path.abspath(from_)
1032+
is_py_limited_api = _extensions_to_py_limited_api.get(from_abs)
1033+
if is_py_limited_api is not None:
1034+
assert bool(self.py_limited_api) == bool(is_py_limited_api), (
1035+
f'Extension was built with'
1036+
f' py_limited_api={is_py_limited_api} but pipcl.Package'
1037+
f' name={self.name!r} has'
1038+
f' py_limited_api={self.py_limited_api}:'
1039+
f' {from_abs!r}'
1040+
)
1041+
9881042
return ret
9891043

9901044

@@ -1519,6 +1573,7 @@ def _fromto(self, p):
15191573
log2(f'returning {from_=} {to_=}')
15201574
return from_, to_
15211575

1576+
_extensions_to_py_limited_api = dict()
15221577

15231578
def build_extension(
15241579
name,
@@ -1629,6 +1684,11 @@ def build_extension(
16291684
py_limited_api:
16301685
If true we build for current Python's limited API / stable ABI.
16311686
1687+
Note that we will assert false if this extension is added to a
1688+
pipcl.Package that has a different <py_limited_api>, because
1689+
on Windows importing a non-py_limited_api extension inside a
1690+
py_limited=True package fails.
1691+
16321692
Returns the leafname of the generated library file within `outdir`, e.g.
16331693
`_{name}.so` on Unix or `_{name}.cp311-win_amd64.pyd` on Windows.
16341694
'''
@@ -1886,6 +1946,8 @@ def build_extension(
18861946
#run(f'ls -l {path_so}', check=0)
18871947
#run(f'file {path_so}', check=0)
18881948

1949+
_extensions_to_py_limited_api[os.path.abspath(path_so)] = py_limited_api
1950+
18891951
return path_so_leaf
18901952

18911953

@@ -2864,6 +2926,9 @@ def log_line_numbers(yes):
28642926
global g_log_line_numbers
28652927
g_log_line_numbers = bool(yes)
28662928

2929+
def log(text='', caller=1):
2930+
_log(text, 0, caller+1)
2931+
28672932
def log0(text='', caller=1):
28682933
_log(text, 0, caller+1)
28692934

@@ -3146,11 +3211,8 @@ def swig_get(swig, quick, swig_local='pipcl-swig-git'):
31463211
# > If you need to have bison first in your PATH, run:
31473212
# > echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
31483213
#
3149-
run(f'brew install bison')
3150-
PATH = os.environ['PATH']
3151-
prefix_bison = run('brew --prefix bison', capture=1).strip()
3152-
PATH = f'{prefix_bison}/bin:{PATH}'
3153-
swig_env_extra = dict(PATH=PATH)
3214+
swig_env_extra = dict()
3215+
macos_add_brew_path('bison', swig_env_extra)
31543216
run(f'which bison')
31553217
run(f'which bison', env_extra=swig_env_extra)
31563218
# Build swig.
@@ -3164,6 +3226,38 @@ def swig_get(swig, quick, swig_local='pipcl-swig-git'):
31643226
return swig
31653227

31663228

3229+
def macos_add_brew_path(package, env=None, gnubin=True):
3230+
'''
3231+
Adds path(s) for Brew <package>'s binaries to env['PATH'].
3232+
3233+
Args:
3234+
package:
3235+
Name of package. We get <package_root> of installed package by
3236+
running `brew --prefix <package>`.
3237+
env:
3238+
The environment dict to modify. If None we use os.environ. If PATH
3239+
is not in <env>, we first copy os.environ['PATH'] into <env>.
3240+
gnubin:
3241+
If true, we also add path to gnu binaries if it exists,
3242+
<package_root>/libexe/gnubin.
3243+
'''
3244+
if not darwin():
3245+
return
3246+
if env is None:
3247+
env = os.environ
3248+
if 'PATH' not in env:
3249+
env['PATH'] = os.environ['PATH']
3250+
package_root = run(f'brew --prefix {package}', capture=1).strip()
3251+
def add(path):
3252+
if os.path.isdir(path):
3253+
log1(f'Adding to $PATH: {path}')
3254+
PATH = env['PATH']
3255+
env['PATH'] = f'{path}:{PATH}'
3256+
add(f'{package_root}/bin')
3257+
if gnubin:
3258+
add(f'{package_root}/libexec/gnubin')
3259+
3260+
31673261
def _show_dict(d):
31683262
ret = ''
31693263
for n in sorted(d.keys()):

0 commit comments

Comments
 (0)