Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f1fd91d
Begun adding RulesHandler
Monarda Jul 18, 2024
4ccfb7c
Implementation and example for nthandlers
Monarda Aug 4, 2024
df902e4
Merge branch 'epics-base:master' into master
Monarda Aug 4, 2024
6c45ed9
Merge branch 'epics-base:master' into master
Monarda Aug 23, 2024
b3b7a36
Merge branch 'master' of github.com:Monarda/p4p
Monarda Jun 23, 2026
ac97302
Merge branch 'master' of github.com:Monarda/p4p
Monarda Jun 24, 2026
800e3ec
Add support for Python stub file generation for _p4p and _gw
Monarda Jun 24, 2026
8e1b84a
Fixes as a result of testing on Linux
Monarda Jun 24, 2026
2a598d7
Move mypy to a dev dependency
Monarda Jun 24, 2026
1dadd13
Remove example leftover from previous work
Monarda Jun 24, 2026
cf3d483
Not required for minimal demo
Monarda Jun 24, 2026
d4fc7a4
Don't version track .pyi files, instead automatically generate them a…
Monarda Jun 27, 2026
621ad67
Relax stubgen version requirement
Monarda Jun 27, 2026
1ad0299
Fix Python 2.7 compatibility
Monarda Jun 27, 2026
b53aaa8
Add mypy to CI/CD pip downloads
Monarda Jun 27, 2026
d9b7979
Still chasing Python 2.7 compatibility!
Monarda Jun 27, 2026
d0baf1c
Another attempt to get Python 2.7 working
Monarda Jun 27, 2026
c331d2d
Make sure .pyi files are in the wheel
Monarda Jun 27, 2026
9a950c0
Minor tidying up
Monarda Jun 27, 2026
3714a7b
Small tidying up to conform to PEP8
Monarda Jun 27, 2026
34a52ff
Fix emdash added by LLM!
Monarda Jun 29, 2026
d14fc67
So hard to get rid of this file!
Monarda Jun 29, 2026
f472700
Merge branch 'epics-base:master' into typing_stubs_automate
Monarda Jun 29, 2026
d1a9544
Merge branch 'typing_stubs_automate' of github.com:Monarda/p4p into t…
Monarda Jun 29, 2026
8b48d02
Switch to using sys.executable
Monarda Jun 29, 2026
d46fa98
Test whether this change will work on Python 2.7
Monarda Jun 29, 2026
f374d3f
Bad syntax; didn't delete orphaned fi
Monarda Jun 29, 2026
d7f69ec
Revert "Test whether this change will work on Python 2.7"
Monarda Jun 29, 2026
63aab2e
Test that this is wrong in CI/CD
Monarda Jun 30, 2026
527bea7
Tidy formating a little more
Monarda Jun 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ jobs:
"${{ matrix.numpy || 'numpy' }}" \
setuptools_dso pvxslibs wheel ply nose2 \
cothread setuptools_scm
if python -c "import sys; sys.exit(0 if sys.version_info >= (3, 8) else 1)"; then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pip understands conditionals already, no need for special machinery.

        "mypy; python_version>='3.8'"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps I'm misunderstanding the syntax here, but isn't this part of build.yml not using the information in pyproject.toml?

Experimenting shows that

pip download $PRE -d output \
                --only-binary numpy,Cython \
                "${{ matrix.cy || 'Cython' }}" \
                "${{ matrix.numpy || 'numpy' }}" \
                setuptools_dso pvxslibs wheel ply nose2 \
                cothread setuptools_scm mypy

produces a GitHub CI/CD error warning of ERROR: You need Python 3.5 or later to use mypy.

I could replace this with pip download . but that would presumably interfere with the matrix?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pip download ... accepts the same syntax as would be found in pyproject.toml.

            pip download $PRE -d output \
                --only-binary numpy,Cython \
                "${{ matrix.cy || 'Cython' }}" \
                "${{ matrix.numpy || 'numpy' }}" \
                setuptools_dso pvxslibs wheel ply nose2 \
                cothread setuptools_scm \
                "mypy; python_version>='3.8'"

@Monarda Monarda Jun 30, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

D'oh! I understand; I've made the change now.

pip download $PRE -d output mypy
fi

# create source tar
pip install $PRE --no-index -f ./output setuptools_dso numpy Cython pvxslibs
Expand Down Expand Up @@ -243,7 +246,7 @@ jobs:
--only-binary numpy,Cython \
"${{ matrix.cy || 'Cython' }}" \
"${{ matrix.numpy || 'numpy' }}" \
setuptools_dso pvxslibs wheel ply nose2
setuptools_dso pvxslibs wheel ply nose2 mypy

export SETUPTOOLS_DSO_PLAT_NAME=${{ matrix.arch }}

Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ include src/*.cpp
include src/p4p/*.pyx
include src/p4p/*.pxd
include src/p4p/pvxs/*.pxd
include src/p4p/*.pyi
include src/p4p/*.conf
include src/p4p/*.service
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ requires = [
"epicscorelibs>=7.0.7.99.1.2a2; python_version>='3.12'",
"pvxslibs>=1.3.1",
"pvxslibs>=1.4.0; python_version>='3.12'",
"mypy>=1.14.1; python_version>='3.8'",
]
179 changes: 173 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
from __future__ import print_function

import os
import shutil
import subprocess
import sys
import sysconfig

from setuptools_dso import Extension, setup, cythonize
from setuptools import Command
from setuptools_dso import Extension, setup, cythonize, build_ext as _build_ext

import numpy

Expand All @@ -31,15 +35,13 @@ def get_numpy_include_dirs():
if get_config_var('CMPLR_CLASS') in ('gcc', 'clang'):
cxxflags += ['-std=c++11']
ldflags = []
import sys
import platform
if sys.platform=='linux2' and not sysconfig.get_config_var('Py_DEBUG'):
if sys.platform.startswith('linux') and not sysconfig.get_config_var('Py_DEBUG'):

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to fix a small bug in the existing versions of setup.py.

sys.platform will return linux on Python3 so this will not trigger. sys.platform.startswith('linux') makes it compatible with Python 2.7 and 3.

# c++ debug symbols size is huge. ~20x code size.
# So we choose to only emit debug symbols when building for an interpreter
# with debugging enabled (aka 'python-dbg' on debian).
cxxflags += ['-g0']

elif platform.system()=='Darwin':
elif sys.platform == 'darwin':

@Monarda Monarda Jun 29, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made consistent with other usages. Removes the need to import platform.

# avoid later failure where install_name_tool may run out of space.
# install_name_tool: changing install names or rpaths can't be redone for:
# ... because larger updated load commands do not fit (the program must be relinked,
Expand All @@ -51,6 +53,170 @@ def get_numpy_include_dirs():
# return c++ types.
cppflags = get_config_var('CPPFLAGS') + [('__PYX_EXTERN_C','extern')]

# Fully-qualified module names for which .pyi stubs are generated.
# Must match the Extension name= values below.
_STUB_MODULES = ['p4p._p4p', 'p4p._gw']

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure whether to use a glob here (*.pyi) or specify the files explicitly. I've taken the more conservative approach of being explicit.



class GenerateStubs(Command):
"""Generate .pyi stub files for compiled extensions using mypy stubgen.

Invoked automatically by build_ext. To regenerate stubs without
rebuilding the extensions::

python setup.py generate_stubs
"""

description = 'generate .pyi stub files for compiled extensions'
user_options = []

def initialize_options(self): pass
def finalize_options(self): pass

def _find_stubgen(self):
"""Locate the stubgen console script installed alongside this Python.

The scripts directory is checked first so that the environment-local
stubgen is preferred over any system-wide installation.
"""
stubgen = shutil.which('stubgen', path=sysconfig.get_path('scripts')) \
or shutil.which('stubgen')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does stubgen have an import-able main()? Like pip ... vs. python -m pip .... Enables use of sys.executable as an entry point, which avoids possible confusion with $PATH referencing other environments.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I've switched to using sys.executable as you suggested which means _find_stubgen() is gone. I also experimented with calling stubgen.main() directly, but it wouldn't be any simpler on Windows due to its using multiprocessing.


if stubgen is None:
print(
"WARNING: stubgen not found; skipping stub generation. "
"Typing stub files (*.pyi) will not be generated",
file=sys.stderr,
)
return None

return stubgen

def _make_env(self):
"""Return a copy of os.environ with pvxslibs/epicscorelibs lib dirs
prepended to the platform DSO search path variable, so that stubgen
subprocesses can load the extensions in editable installs where the
relative RUNPATH embedded in the .so/.pyd does not resolve correctly.
"""
# pvxslibs exposes no public lib_path attribute;
# path derived by convention.
lib_dirs = [d for d in [
os.path.join(os.path.dirname(pvxslibs.__file__), 'lib'),
epicscorelibs.path.lib_path,
] if os.path.isdir(d)]
env = os.environ.copy()
if sys.platform == 'win32':
path_var = 'PATH'
elif sys.platform == 'darwin':
path_var = 'DYLD_LIBRARY_PATH'
else:
path_var = 'LD_LIBRARY_PATH'
existing = env.get(path_var, '')
env[path_var] = os.pathsep.join(
lib_dirs + ([existing] if existing else [])
)

return env

@staticmethod
def _stub_src_path(mod):
"""Return the source-tree path for a module's generated .pyi file."""
return os.path.join('src', mod.replace('.', os.sep) + '.pyi')

def run(self):
"""Verify extensions are importable, then invoke stubgen to write
.pyi files.
"""
stubgen = self._find_stubgen()
if stubgen is None:
return
env = self._make_env()

# For non-inplace builds (e.g. wheel), extensions land in
# build_lib rather than the source tree, so add it to PYTHONPATH
# for the subprocesses.
build_ext_cmd = self.get_finalized_command('build_ext')
if not build_ext_cmd.inplace:
existing = env.get('PYTHONPATH', '')
env['PYTHONPATH'] = os.pathsep.join(
[build_ext_cmd.build_lib] + ([existing] if existing else [])
)

# Verify extensions are importable under the same env stubgen will use.
# Done in a subprocess so the current process's DSO search path (which
# may not include pvxslibs/lib for editable installs) is not a factor.
for mod in _STUB_MODULES:
check = subprocess.run(
[sys.executable, '-c', 'import %s' % mod],
env=env, capture_output=True, check=False,
)
if check.returncode:
print(
"WARNING: extension %r not importable; "
"skipping stub generation. Build it first with:\n"
" python setup.py build_ext --inplace" % mod,
file=sys.stderr,
)
return

# Run stubgen in a subprocess for DSO search path isolation --
# the same reason as the importability check above.
cmd = [stubgen, '--include-docstrings', '--output', 'src']
for mod in _STUB_MODULES:
cmd += ['-m', mod]

result = subprocess.run(cmd, env=env, check=False)
if result.returncode:
print(
"WARNING: stubgen failed with code %s" % result.returncode,
file=sys.stderr,
)
return

missing = [
path for path in
(self._stub_src_path(mod) for mod in _STUB_MODULES)
if not os.path.isfile(path)
]
if missing:
print(
"WARNING: expected stubs not found:\n" + '\n'.join(missing),
file=sys.stderr,
)
return

# build_py runs before build_ext, so *.pyi files generated above
# are not yet present when package_data is copied into build_lib.
# Copy them now.
if not build_ext_cmd.inplace:
for mod in _STUB_MODULES:
stub_src = self._stub_src_path(mod)
if os.path.isfile(stub_src):
stub_dst = os.path.join(
build_ext_cmd.build_lib,
mod.replace('.', os.sep) + '.pyi',
)
shutil.copy2(stub_src, stub_dst)


class BuildExt(_build_ext):
"""Extends the standard build_ext to invoke generate_stubs
after building.
"""

def run(self):
_build_ext.run(self)
if sys.version_info >= (3, 0):
print("Generating .pyi stubs...", file=sys.stderr)
self.run_command('generate_stubs')
else:
print(
"WARNING: skipping .pyi stub generation; "
"Python 2 not supported.",
file=sys.stderr,
)


exts = cythonize([
Extension(
name='p4p._p4p',
Expand Down Expand Up @@ -145,7 +311,8 @@ def get_numpy_include_dirs():
'p4p.asLib',
],
package_dir={'':'src'},
package_data={'p4p': ['*.conf', '*.service']},
package_data={'p4p': ['*.conf', '*.service', '*.pyi']},
cmdclass={'build_ext': BuildExt, 'generate_stubs': GenerateStubs},
ext_modules = exts,
install_requires = install_requires,
extras_require={
Expand Down
1 change: 1 addition & 0 deletions src/p4p/_gw.pyx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# distutils: language = c++
#cython: language_level=2
#cython: binding=True

cimport cython

Expand Down
1 change: 1 addition & 0 deletions src/p4p/_p4p.pyx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# distutils: language = c++
#cython: language_level=2
#cython: binding=True

cimport cython

Expand Down