1
0
mirror of https://github.com/python-LimeReport/python-LimeReport.git synced 2025-09-01 15:53:43 +03:00

prepare the project for distribution

This commit is contained in:
Maxim Slipenko 2023-07-29 11:39:17 -07:00
parent c313f5a946
commit 676a6be463
7 changed files with 698 additions and 9 deletions

View File

@ -3,9 +3,15 @@
"image": "ghcr.io/python-limereport/devcontainer:Qt-6.4.2-Python-3.9",
"customizations": {
"vscode": {
"extensions": [],
"extensions": [
"ms-python.python",
"ms-python.autopep8"
],
"settings": {
"cmake.configureOnOpen": false
"cmake.configureOnOpen": false,
"[python]": {
"editor.defaultFormatter": "ms-python.autopep8"
},
}
}
}

179
.gitignore vendored
View File

@ -1,5 +1,5 @@
# Created by https://www.toptal.com/developers/gitignore/api/cmake,qt,c++
# Edit at https://www.toptal.com/developers/gitignore?templates=cmake,qt,c++
# Created by https://www.toptal.com/developers/gitignore/api/cmake,qt,c++,python
# Edit at https://www.toptal.com/developers/gitignore?templates=cmake,qt,c++,python
### C++ ###
# Prerequisites
@ -52,6 +52,177 @@ _deps
# External projects
*-prefix/
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
### Qt ###
# C++ objects and libs
*.so.*
@ -98,6 +269,4 @@ CMakeLists.txt.user*
*_qmlcache.qrc
# End of https://www.toptal.com/developers/gitignore/api/cmake,qt,c++
build
# End of https://www.toptal.com/developers/gitignore/api/cmake,qt,c++,python

View File

@ -9,7 +9,9 @@ project(LimeReport6)
# LimeReport options
set(USE_QT6 ON)
set(ENABLE_ZINT ON)
set(ENABLE_ZINT OFF)
#
set(LIMEREPORT_STATIC ON)
if (NOT DEFINED PYSIDE_INSTALL_DIR)
set(PYSIDE_INSTALL_DIR $ENV{PYSIDE_INSTALL_DIR})
@ -126,4 +128,13 @@ endforeach()
# TODO:
target_include_directories(${PROJECT_NAME} PRIVATE /home/vscode/pyside-setup/sources/pyside6/PySide6)
set_target_properties(${PROJECT_NAME} PROPERTIES BUILD_RPATH "$ORIGIN/")
set_target_properties(${PROJECT_NAME} PROPERTIES BUILD_RPATH "$ORIGIN/:$ORIGIN/PySide6/:$ORIGIN/shiboken6/")
# ===============
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${PYTHON_EXECUTABLE}
${CMAKE_SOURCE_DIR}/build_scripts/pyi_generator.py
$<TARGET_FILE:${PROJECT_NAME}>
COMMENT "Running pyi_generator")

17
README.md Normal file
View File

@ -0,0 +1,17 @@
Python bindings for [LimeReport](https://github.com/fralx/LimeReport)
🚧 work in progress 🚧
## Build
Default:
```
$ python setup.py build --parallel $(nproc) bdist_wheel
```
With zint (for barcodes):
```
$ LIMEREPORT_USE_ZINT=TRUE python setup.py build --parallel $(nproc) bdist_wheel
```

View File

@ -0,0 +1,309 @@
LICENSE_TEXT = """
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
"""
"""
pyi_generator.py
This script generates .pyi files for arbitrary modules.
"""
import argparse
import io
import logging
import os
import re
import sys
import typing
from pathlib import Path
from contextlib import contextmanager
from textwrap import dedent
from shiboken6 import Shiboken
from shibokensupport.signature.lib.enum_sig import HintingEnumerator
from shibokensupport.signature.lib.tool import build_brace_pattern
import PySide6
# Can we use forward references?
USE_PEP563 = sys.version_info[:2] >= (3, 7)
indent = " " * 4
class Writer(object):
def __init__(self, outfile, *args):
self.outfile = outfile
self.history = [True, True]
def print(self, *args, **kw):
# controlling too much blank lines
if self.outfile:
if args == () or args == ("",):
# We use that to skip too many blank lines:
if self.history[-2:] == [True, True]:
return
print("", file=self.outfile, **kw)
self.history.append(True)
else:
print(*args, file=self.outfile, **kw)
self.history.append(False)
class Formatter(Writer):
"""
Formatter is formatting the signature listing of an enumerator.
It is written as context managers in order to avoid many callbacks.
The separation in formatter and enumerator is done to keep the
unrelated tasks of enumeration and formatting apart.
"""
def __init__(self, outfile, options, *args):
self.options = options
Writer.__init__(self, outfile, *args)
# patching __repr__ to disable the __repr__ of typing.TypeVar:
"""
def __repr__(self):
if self.__covariant__:
prefix = '+'
elif self.__contravariant__:
prefix = '-'
else:
prefix = '~'
return prefix + self.__name__
"""
def _typevar__repr__(self):
return f"typing.{self.__name__}"
typing.TypeVar.__repr__ = _typevar__repr__
# Adding a pattern to substitute "Union[T, NoneType]" by "Optional[T]"
# I tried hard to replace typing.Optional by a simple override, but
# this became _way_ too much.
# See also the comment in layout.py .
brace_pat = build_brace_pattern(3, ",")
pattern = fr"\b Union \s* \[ \s* {brace_pat} \s*, \s* NoneType \s* \]"
replace = r"Optional[\1]"
optional_searcher = re.compile(pattern, flags=re.VERBOSE)
def optional_replacer(source):
return optional_searcher.sub(replace, str(source))
self.optional_replacer = optional_replacer
# self.level is maintained by enum_sig.py
# self.is_method() is true for non-plain functions.
def section(self):
if self.level == 0:
self.print()
self.print()
@contextmanager
def module(self, mod_name):
self.mod_name = mod_name
txt = f"""\
# Module `{mod_name}`
<<IMPORTS>>
"""
self.print(dedent(txt))
yield
@contextmanager
def klass(self, class_name, class_str):
spaces = indent * self.level
while "." in class_name:
class_name = class_name.split(".", 1)[-1]
class_str = class_str.split(".", 1)[-1]
if self.have_body:
self.print(f"{spaces}class {class_str}:")
else:
self.print(f"{spaces}class {class_str}: ...")
yield
@contextmanager
def function(self, func_name, signature, decorator=None):
if func_name == "__init__":
self.print()
key = func_name
spaces = indent * self.level
if isinstance(signature, list):
for sig in signature:
self.print(f'{spaces}@overload')
self._function(func_name, sig, spaces)
else:
self._function(func_name, signature, spaces, decorator)
if func_name == "__init__":
self.print()
yield key
def _function(self, func_name, signature, spaces, decorator=None):
if decorator:
# In case of a PyClassProperty the classmethod decorator is not used.
self.print(f'{spaces}@{decorator}')
elif self.is_method() and "self" not in signature.parameters:
kind = "class" if "cls" in signature.parameters else "static"
self.print(f'{spaces}@{kind}method')
signature = self.optional_replacer(signature)
self.print(f'{spaces}def {func_name}{signature}: ...')
@contextmanager
def enum(self, class_name, enum_name, value):
spaces = indent * self.level
hexval = hex(value)
self.print(f"{spaces}{enum_name:25}: {class_name} = ... # {hexval}")
yield
def find_imports(text):
return [imp for imp in PySide6.__all__ if f"PySide6.{imp}." in text]
FROM_IMPORTS = [
(None, ["builtins"]),
(None, ["os"]),
(None, ["enum"] if sys.pyside63_option_python_enum else []),
("typing", typing.__all__),
("PySide6.QtCore", ["PyClassProperty"]),
("shiboken6", ["Shiboken"]),
]
def filter_from_imports(from_struct, text):
"""
Build a reduced new `from` structure (nfs) with found entries, only
"""
nfs = []
for mod, imports in from_struct:
lis = []
nfs.append((mod, lis))
for each in imports:
if re.search(rf"(\b|@){each}\b([^\s\(:]|\n)", text):
lis.append(each)
if not lis:
nfs.pop()
return nfs
def find_module(import_name, outpath, from_pyside):
"""
Find a module either directly by import, or use the full path,
add the path to sys.path and import then.
"""
if from_pyside:
# internal mode for generate_pyi.py
plainname = import_name.split(".")[-1]
outfilepath = Path(outpath) / f"{plainname}.pyi"
return import_name, plainname, outfilepath
# we are alone in external module mode
p = Path(import_name).resolve()
if not p.exists():
raise ValueError(f"File {p} does not exist.")
if not outpath:
outpath = p.parent
# temporarily add the path and do the import
sys.path.insert(0, os.fspath(p.parent))
plainname = p.name.split(".")[0]
__import__(plainname)
sys.path.pop(0)
return plainname, plainname, Path(outpath) / (plainname + ".pyi")
def generate_pyi(import_name, outpath, options):
"""
Generates a .pyi file.
"""
import_name, plainname, outfilepath = find_module(import_name, outpath, options._pyside_call)
top = __import__(import_name)
obj = getattr(top, plainname) if import_name != plainname else top
if not getattr(obj, "__file__", None) or Path(obj.__file__).is_dir():
raise ModuleNotFoundError(f"We do not accept a namespace as module `{plainname}`")
module = sys.modules[import_name]
outfile = io.StringIO()
fmt = Formatter(outfile, options)
fmt.print(LICENSE_TEXT.strip())
need_imports = options._pyside_call and not USE_PEP563
if USE_PEP563:
fmt.print("from __future__ import annotations")
fmt.print()
fmt.print(dedent(f'''\
"""
This file contains the exact signatures for all functions in module
{import_name}, except for defaults which are replaced by "...".
"""
'''))
HintingEnumerator(fmt).module(import_name)
fmt.print("# eof")
# Postprocess: resolve the imports
if options._pyside_call:
global PySide6
import PySide6
with outfilepath.open("w") as realfile:
wr = Writer(realfile)
outfile.seek(0)
while True:
line = outfile.readline()
if not line:
break
line = line.rstrip()
# we remove the "<<IMPORTS>>" marker and insert imports if needed
if line == "<<IMPORTS>>":
text = outfile.getvalue()
wr.print("import " + import_name)
for mod_name in find_imports(text):
imp = "PySide6." + mod_name
if imp != import_name:
wr.print("import " + imp)
wr.print()
for mod, imports in filter_from_imports(FROM_IMPORTS, text):
import_args = ', '.join(imports)
if mod is None:
# special case, a normal import
wr.print(f"import {import_args}")
else:
wr.print(f"from {mod} import {import_args}")
wr.print()
wr.print()
else:
wr.print(line)
if not options.quiet:
options.logger.info(f"Generated: {outfilepath}")
# PYSIDE-1735: .pyi files are no longer compatible with Python, because
# enum classes contain ellipsis which a Python enum forbids.
# We will implement tests with Mypy, instead.
def main():
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description=dedent("""\
pyi_generator.py
----------------
This script generates the .pyi file for an arbitrary module.
You pass in the full path of a compiled, importable module.
pyi_generator will try to generate an interface "<module>.pyi".
"""))
parser.add_argument("module",
help="The full path name of an importable module binary (.pyd, .so)")
parser.add_argument("--quiet", action="store_true", help="Run quietly")
parser.add_argument("--check", action="store_true", help="Test the output")
parser.add_argument("--outpath",
help="the output directory (default = location of module binary)")
options = parser.parse_args()
module = options.module
outpath = options.outpath
qtest_env = os.environ.get("QTEST_ENVIRONMENT", "")
logging.basicConfig(level=logging.DEBUG if qtest_env else logging.INFO)
logger = logging.getLogger("pyi_generator")
if outpath and not Path(outpath).exists():
os.makedirs(outpath)
logger.info(f"+++ Created path {outpath}")
options._pyside_call = False
options.is_ci = qtest_env == "ci"
options.logger = logger
generate_pyi(module, outpath, options=options)
if __name__ == "__main__":
main()
# eof

8
setup.cfg Normal file
View File

@ -0,0 +1,8 @@
[metadata]
version = 1.7.4
home_page = https://github.com/python-LimeReport/python-LimeReport
description = Python bindings for LimeReport
author = Maxim Slipenko
author_email = python-limereport@maks1ms.anonaddy.com
long_description = file: README.md
long_description_content_type = text/markdown

169
setup.py Normal file
View File

@ -0,0 +1,169 @@
import os
import re
import subprocess
import sys
from pathlib import Path
import platform
from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext
from wheel.bdist_wheel import bdist_wheel
USE_ZINT_ENV_KEY = 'LIMEREPORT_USE_ZINT'
USE_ZINT = os.environ.get(USE_ZINT_ENV_KEY, False)
def qt_version_tuple():
proc = subprocess.Popen(['qmake', '-query', 'QT_VERSION'], stdout=subprocess.PIPE)
output = proc.stdout.read()
return output.decode('utf-8').strip().split('.')
qt_major = qt_version_tuple()[0]
(p_major, p_minor, p_patchlevel) = platform.python_version_tuple()
pyside_version = f"{(2 if qt_major == 5 else qt_major)}"
def get_name():
n = f"LimeReport{pyside_version}"
if USE_ZINT:
return n + "-Z"
return n
def get_license():
if USE_ZINT:
return "GPLv3"
else:
return "LGPLv3"
def get_license_files():
if USE_ZINT:
return ["COPYING.gpl3"]
else:
return ["COPYING.lgpl3"]
def get_classifiers():
c = [
"Environment :: X11 Applications :: Qt",
"Programming Language :: C++",
f"Programming Language :: Python :: {p_major}",
f"Programming Language :: Python :: {p_major}.{p_minor}",
]
if USE_ZINT:
c.append("License :: OSI Approved :: GNU General Public License v3 (GPLv3)")
else:
c.append("License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)")
return c
PLAT_TO_CMAKE = {
"win32": "Win32",
"win-amd64": "x64",
"win-arm32": "ARM",
"win-arm64": "ARM64",
}
class CMakeExtension(Extension):
def __init__(self, name: str, sourcedir: str = "", py_limited_api = False) -> None:
super().__init__(
name=name,
sources=[],
py_limited_api=py_limited_api,
)
self.sourcedir = os.fspath(Path(sourcedir).resolve())
class BuildExt(build_ext):
def build_extension(self, ext: CMakeExtension) -> None:
# Must be in this form due to bug in .resolve() only fixed in Python 3.10+
ext_fullpath = Path.cwd() / self.get_ext_fullpath(ext.name)
extdir = ext_fullpath.parent.resolve()
debug = int(os.environ.get("DEBUG", 0)) if self.debug is None else self.debug
cfg = "Debug" if debug else "Release"
cmake_generator = os.environ.get("CMAKE_GENERATOR", "")
cmake_args = [
f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}{os.sep}",
f"-DCMAKE_BUILD_TYPE={cfg}", # not used on MSVC, but no harm
f"-DENABLE_ZINT={USE_ZINT}"
]
build_args = []
# Adding CMake arguments set as environment variable
# (needed e.g. to build for ARM OSx on conda-forge)
if "CMAKE_ARGS" in os.environ:
cmake_args += [item for item in os.environ["CMAKE_ARGS"].split(" ") if item]
if self.compiler.compiler_type != "msvc":
if not cmake_generator or cmake_generator == "Ninja":
try:
import ninja
ninja_executable_path = Path(ninja.BIN_DIR) / "ninja"
cmake_args += [
"-GNinja",
f"-DCMAKE_MAKE_PROGRAM:FILEPATH={ninja_executable_path}",
]
except ImportError:
pass
else:
# Single config generators are handled "normally"
single_config = any(x in cmake_generator for x in {"NMake", "Ninja"})
# CMake allows an arch-in-generator style for backward compatibility
contains_arch = any(x in cmake_generator for x in {"ARM", "Win64"})
# Specify the arch if using MSVC generator, but only if it doesn't
# contain a backward-compatibility arch spec already in the
# generator name.
if not single_config and not contains_arch:
cmake_args += ["-A", PLAT_TO_CMAKE[self.plat_name]]
# Multi-config generators have a different way to specify configs
if not single_config:
cmake_args += [
f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{cfg.upper()}={extdir}"
]
build_args += ["--config", cfg]
if sys.platform.startswith("darwin"):
# Cross-compile support for macOS - respect ARCHFLAGS if set
archs = re.findall(r"-arch (\S+)", os.environ.get("ARCHFLAGS", ""))
if archs:
cmake_args += ["-DCMAKE_OSX_ARCHITECTURES={}".format(";".join(archs))]
if "CMAKE_BUILD_PARALLEL_LEVEL" not in os.environ:
if hasattr(self, "parallel") and self.parallel:
build_args += [f"-j{self.parallel}"]
build_temp = Path(self.build_temp) / ext.name
if not build_temp.exists():
build_temp.mkdir(parents=True)
subprocess.run(
["cmake", ext.sourcedir, *cmake_args], cwd=build_temp, check=True
)
subprocess.run(
["cmake", "--build", ".", *build_args], cwd=build_temp, check=True
)
setup(
name=get_name(),
license = get_license(),
classifiers = get_classifiers(),
license_files = get_license_files(),
ext_modules=[
CMakeExtension(
f"LimeReport{qt_major}",
py_limited_api=True
)
],
cmdclass={
"build_ext": BuildExt,
},
install_requires=[
f"PySide{pyside_version}"
],
)