File setup.py#
File List > libpisoundmicro > pypisoundmicro > setup.py
Go to the documentation of this file
#!/usr/bin/env python3
from setuptools import find_packages, setup, Extension
from setuptools.command.build_py import build_py
import sys
import re
import os
import glob
import importlib.util
import shutil
import ast
# Find the library path
lib_paths = []
for search_path in ['../debian/tmp/usr/lib', '../debian/libpisoundmicro/usr/lib', '../']:
for lib_glob in glob.glob(f"{search_path}/**/libpisoundmicro.*", recursive=True):
lib_dir = os.path.dirname(lib_glob)
if lib_dir not in lib_paths:
lib_paths.append(lib_dir)
if '--owner=root' in sys.argv:
sys.argv.remove('--owner=root')
if '--group=root' in sys.argv:
sys.argv.remove('--group=root')
class DocstringInjector(ast.NodeTransformer):
def __init__(self, psm_module):
self.psm = psm_module
self.modified = False
self.processed_count = 0
def visit_FunctionDef(self, node):
# Process decorators
copy_doc_target = None
new_decorators = []
for decorator in node.decorator_list:
if (isinstance(decorator, ast.Call) and
isinstance(decorator.func, ast.Name) and
decorator.func.id == 'copy_doc'):
# Extract the path from copy_doc(psm.X.Y.Z)
if len(decorator.args) == 1:
# Extract the full path
attr_path = self._extract_attribute_path(decorator.args[0])
if attr_path.startswith('psm.'):
copy_doc_target = attr_path[4:] # Remove 'psm.' prefix
self.modified = True
continue # Skip this decorator
new_decorators.append(decorator)
# Update decorators list (removing @copy_doc)
node.decorator_list = new_decorators
# If we found a copy_doc decorator, apply the docstring
if copy_doc_target:
try:
# Navigate to source object and get its docstring
obj = self.psm
for part in copy_doc_target.split('.'):
obj = getattr(obj, part)
docstring = obj.__doc__ or ''
# Set the docstring on the function
node.body = [ast.Expr(value=ast.Constant(value=docstring))] + node.body
self.processed_count += 1
print(f"Copied docstring from {copy_doc_target} to {node.name}")
except Exception as e:
print(f"Error copying docstring for {node.name}: {e}")
return self.generic_visit(node)
def _extract_attribute_path(self, node):
"""Extract the full attribute path (e.g., psm.Audio.init)"""
if isinstance(node, ast.Name):
return node.id
elif isinstance(node, ast.Attribute):
return f"{self._extract_attribute_path(node.value)}.{node.attr}"
return ""
def preprocess_single_file(file_path, dest_file, psm_module):
"""
Preprocess a single Python file by injecting docstrings from SWIG module.
Args:
file_path: Path to the source Python file
dest_file: Path where the processed file will be written
psm_module: The imported SWIG module with docstrings
Returns:
int: Number of docstrings processed
"""
# Read the source file
with open(file_path, 'r') as f:
source = f.read()
# Parse the source into an AST
tree = ast.parse(source)
# Apply the docstring injector
transformer = DocstringInjector(psm_module)
modified_tree = transformer.visit(tree)
ast.fix_missing_locations(modified_tree)
# Convert modified AST back to source
modified_source = ast.unparse(modified_tree)
# If copy_doc was used in this file, remove the decorator definition
if transformer.modified:
# This pattern removes the copy_doc decorator function definition
import re
modified_source = re.sub(
r"def copy_doc\(from_func\):\n\s+def decorator\(to_func\):.+?return decorator\n\n",
"",
modified_source,
flags=re.DOTALL
)
# Create destination directory if needed
os.makedirs(os.path.dirname(dest_file), exist_ok=True)
# Write the modified source to destination
with open(dest_file, 'w') as f:
f.write(modified_source)
return transformer.processed_count
def find_and_preprocess_files(self):
"""
Find all Python files in pypisoundmicro/ (excluding swig/) and preprocess them.
Returns:
tuple: (number of files processed, total number of docstrings injected)
"""
print("Finding and preprocessing Python files in pypisoundmicro/...")
# Find all Python files in pypisoundmicro/ but not in pypisoundmicro/swig/
source_dir = 'pypisoundmicro'
py_files = []
for root, dirs, files in os.walk(source_dir):
if 'swig' in root.split(os.path.sep):
continue # Skip swig subdirectory
for file in files:
if file.endswith('.py') and not file.startswith('__'):
py_files.append(os.path.join(root, file))
return py_files
def preprocess_python_files(self):
print("Preprocessing Python files in pypisoundmicro/ to inject docstrings...")
try:
# Find the built SWIG module
build_dirs = glob.glob(os.path.join(self.build_lib, 'pypisoundmicro', 'swig', '_pypisoundmicro*.so'))
if not build_dirs:
print("Warning: Could not find built SWIG module, skipping docstring injection")
return
# Import the SWIG module to access docstrings
sys.path.insert(0, self.build_lib)
from pypisoundmicro.swig import pypisoundmicro as psm
# Find Python files to process
py_files = find_and_preprocess_files(self)
processed_total = 0
# Process each file
for py_file in py_files:
# Define the destination file path
rel_path = os.path.relpath(py_file, start='.')
dest_file = os.path.join(self.build_lib, rel_path)
# Process the file
processed_count = preprocess_single_file(py_file, dest_file, psm)
processed_total += processed_count
if processed_count > 0:
print(f"Injected {processed_count} docstrings into {rel_path}")
print(f"Successfully processed {len(py_files)} files with {processed_total} total docstrings injected")
except Exception as e:
print(f"Error preprocessing Python files with AST: {e}")
import traceback
traceback.print_exc()
# Ensure all files are still copied even if processing fails
for py_file in glob.glob(os.path.join('pypisoundmicro', '**', '*.py'), recursive=True):
if 'swig' not in py_file.split(os.path.sep):
rel_path = os.path.relpath(py_file, start='.')
dest_file = os.path.join(self.build_lib, rel_path)
os.makedirs(os.path.dirname(dest_file), exist_ok=True)
shutil.copy2(py_file, dest_file)
# Custom build_py command to preprocess Python files
class CustomBuildPy(build_py):
def run(self):
super().run()
preprocess_python_files(self)
pisoundmicro_module = Extension(
'pypisoundmicro.swig._pypisoundmicro',
sources = [ 'pisoundmicro.i' ],
swig_opts = [ '-c++', '-py3', '-includeall', '-I../include', '-I/usr/include', '-doxygen', '-outdir', 'pypisoundmicro/swig' ],
include_dirs = [ '../include' ],
library_dirs = lib_paths,
libraries = [ 'pisoundmicro' ]
)
VERSION = '1.0.0-dev'
try:
with open("VERSION") as f:
VERSION = f.read().strip()
except:
pass
setup(
name='pypisoundmicro',
version=VERSION,
packages=find_packages(),
description='Python bindings for libpisoundmicro',
author='Giedrius Trainavičius',
author_email = 'giedrius@blokas.io',
url='https://blokas.io/',
license='LGPLv3',
ext_modules=[pisoundmicro_module],
py_modules=['pypisoundmicro'],
cmdclass={
'build_py': CustomBuildPy,
},
)