ADDED html/depends.html Index: html/depends.html ================================================================== --- html/depends.html +++ html/depends.html @@ -0,0 +1,166 @@ + + + + + + +pluginconf.depends API documentation + + + + + + + + + + + +
+
+
+

Module pluginconf.depends

+
+
+

Dependency validation and consistency checker for updates

+
+
+
+
+
+
+
+
+

Classes

+
+
+class DependencyValidation +(add={}, core=['st2', 'uikit', 'config', 'action']) +
+
+

Now this definitely requires customization. Each plugin can carry +a list of (soft-) dependency names.

+

# depends: config, appcore >= 2.0, bin:wkhtmltoimage, python < 3.5

+

Here only in-application modules are honored, system references +ignored. Unknown plugin names are also skipped. A real install +helper might want to auto-tick them on, etc. This example is just +meant for probing downloadable plugins.

+

The .valid() helper only asserts the api: string, or skips existing +modules, and if they're more recent. +While .depends() compares minimum versions against existing modules.

+

In practice there's little need for full-blown dependency resolving +for application-level modules.

+

Attributes

+
+
api : list
+
allowed api: identifiers for .valid() stream checks
+
log : logging
+
warning handler
+
have : dict
+
accumulated list of existing/virtual plugins
+
+

Parameters

+
+
add : dict
+
name to pmd list of existing/core/virtual plugins (can define +versions or own dependencies)
+
core : list
+
name list of virtual plugins
+
+

Class variables

+
+
var api
+
+

debugging

+
+
var log
+
+
+
+
+

Methods

+
+
+def and_or(self, deps, have, r=True) +
+
+
+
+
+def cmp(self, d, have, absent=True) +
+
+
+
+
+def depends(self, plugin) +
+
+

test depends: and breaks:

+
+
+def module_test(self, type, name) +
+
+
+
+
+def neither(self, deps, have) +
+
+
+
+
+def split(self, dep_str) +
+
+
+
+
+def valid(self, new_plugin) +
+
+

check plugin info from repository stream (fields there $name, $file, $dist, api, id, depends, etc)

+
+
+
+
+
+
+ +
+ + + ADDED html/flit.html Index: html/flit.html ================================================================== --- html/flit.html +++ html/flit.html @@ -0,0 +1,75 @@ + + + + + + +pluginconf.flit API documentation + + + + + + + + + + + +
+
+
+

Module pluginconf.flit

+
+
+

monkeypatches flint to use pluginconf sources for packaging

+
+
+
+
+
+
+

Functions

+
+
+def inject(where) +
+
+

monkeypatch into module

+
+
+def pmd_meta(pmd, ini) +
+
+

enjoin PMD fields with flit meta data

+
+
+
+
+
+
+ +
+ + + ADDED html/gui.html Index: html/gui.html ================================================================== --- html/gui.html +++ html/gui.html @@ -0,0 +1,160 @@ + + + + + + +pluginconf.gui API documentation + + + + + + + + + + + +
+
+
+

Module pluginconf.gui

+
+
+

PySimpleGUI window to populate config dict via plugin options

+
+
+
+
+
+
+

Functions

+
+
+def option_entry(o, config) +
+
+
+
+
+def plugin_entry(e, plugin_states) +
+
+
+
+
+def plugin_layout(ls, config, plugin_states, opt_label=False) +
+
+
+
+
+def read_options(files) +
+
+
+
+
+def window(config={}, plugin_states={}, files=['*/*.py'], plugins={}, opt_label=False, theme='DefaultNoMoreNagging', **kwargs) +
+
+

Reads *.py files and crafts a settings dialog from meta data.

+

Parameters

+
+
config : dict
+
Config settings, updated after dialog completion
+
plugin_states : dict
+
Plugin activation states, also input/output
+
files : list
+
Glob list of *.py files to extract meta definitions from
+
plugins : dict
+
Alternatively to files=[] list, a preparsed list of pluginmeta+config dicts can be injected
+
opt_label : bool
+
Show config name= as label
+
**kwargs : dict
+
Other options are passed on to PySimpleGUI
+
+
+
+def wrap(s, w=50) +
+
+
+
+
+
+
+

Classes

+
+
+class cast +
+
+
+

Static methods

+
+
+def bool(v) +
+
+
+
+
+def fromtype(v, opt) +
+
+
+
+
+def int(v) +
+
+
+
+
+
+
+
+
+ +
+ + + ADDED html/index.html Index: html/index.html ================================================================== --- html/index.html +++ html/index.html @@ -0,0 +1,174 @@ + + + + + + +pluginconf API documentation + + + + + + + + + + + +
+
+
+

Package pluginconf

+
+
+

Plugin meta extraction and module lookup

+
+
+

Sub-modules

+
+
pluginconf.depends
+
+

Dependency validation and consistency checker for updates

+
+
pluginconf.flit
+
+

monkeypatches flint to use pluginconf sources for packaging

+
+
pluginconf.gui
+
+

PySimpleGUI window to populate config dict via plugin options

+
+
pluginconf.setup
+
+

Simulates setuptools.setup()

+
+
+
+
+
+
+

Functions

+
+
+def add_plugin_defaults(conf_options, conf_plugins, meta, module='') +
+
+

Utility function which collect defaults from plugin meta data to +a config store. Which in the case of streamtuner2 is really just a +dictionary conf{} and a plugin list in conf.plugins{}.

+

Parameters

+
+
conf_options : dict : input/output
+
storage for amassed options
+
conf_plugins : dict : input/output
+
enable status based on plugin state/priority:
+
meta : dict
+
input plugin meta data (invoke once per plugin)
+
module : str
+
basename of meta: blocks plugin file
+
+
+
+def get_data(filename, decode=False, gzip=False, file_base=None) +
+
+

Fetches file content from install path or from within PYZ +archive. This is just an alias and convenience wrapper for +pkgutil.get_data(). +Utilizes the module_base / plugin_base as top-level reference.

+

Parameters

+
+
filename :  str
+
filename in pyz or bundle
+
decode : bool
+
text file decoding utf-8
+
gzip : bool
+
automatic gzdecode
+
file_base : list
+
alternative base module reference
+
+

Returns

+
+
str : file contents
+
 
+
+
+
+def module_list(extra_paths=None) +
+
+

Search through ./plugins/ (and other configured plugin_base +names → paths) and get module basenames.

+

Parameters

+
+
extra_paths : list
+
in addition to plugin_base list
+
+

Returns

+
+
list : names of found plugins
+
 
+
+
+
+def plugin_meta(filename=None, src=None, module=None, frame=1, **kwargs) +
+
+

Extract plugin meta data block from different sources:

+

Parameters

+
+
filename : str
+
read literal files, or .pyz contents
+
src : str
+
from already uncovered script code
+
module : str
+
lookup per pkgutil, from plugin_base or top-level modules
+
frame : int
+
extract comment header of caller (default)
+
extra_base : list
+
additional search directories
+
max_length : list
+
maximum size to read from files
+
+

Returns

+
+
dict : key-value pairs of comment fields, config: preparsed
+
 
+
+
+
+
+
+
+
+ +
+ + + ADDED html/setup.html Index: html/setup.html ================================================================== --- html/setup.html +++ html/setup.html @@ -0,0 +1,198 @@ + + + + + + +pluginconf.setup API documentation + + + + + + + + + + + +
+
+
+

Module pluginconf.setup

+
+
+

Simulates setuptools.setup()

+
+
+
+
+
+
+

Functions

+
+
+def get_readme() +
+
+

get README.md contents

+
+
+def name_to_fn(name) +
+
+

find primary entry point.py from package name

+
+
+def setup(debug=0, **kwargs) +
+
+

Wrapper around setuptools.setup() which adds some defaults +and plugin meta data import, with two shortcut params:

+

Parameters

+
+
fn : str
+
main file "pkg/main.py"
+
long_description : str
+
e.g. "README.md", else comment block used
+
+

Other setup() params work as usual, and are passed trough. Notably +entry_points= or data_files= can be used, even if they get augmented.

+
+
+
+
+

Classes

+
+
+class MetaUtils +(*args, **kwargs) +
+
+

convenience access to PMD fields

+

Ancestors

+
    +
  • builtins.dict
  • +
+

Static methods

+
+
+def datafiles_man() +
+
+

data_files=

+
+
+

Methods

+
+
+def classifiers(self) +
+
+

classifiers: / keywords: / category:

+
+
+def entry_points(self) +
+
+

collect console-scripts:

+
+
+def extras_require(self) +
+
+

suggest: line

+
+
+def get_keywords(self) +
+
+

keywords=

+
+
+def install_requires(self) +
+
+

depends: python:module, pip:module

+
+
+def plugin_doc(self) +
+
+

use comment block

+
+
+def project_urls(self, exclude=('url', 'update')) +
+
+

other-url: https://...

+
+
+def python_requires(self) +
+
+

depends: python >= 3.5

+
+
+def trove_license(self) +
+
+

license: to License ::

+
+
+def trove_status(self) +
+
+

state: to DevStatus ::

+
+
+
+
+
+
+ +
+ + + Index: pluginconf/__init__.py ================================================================== --- pluginconf/__init__.py +++ pluginconf/__init__.py @@ -160,14 +160,24 @@ Fetches file content from install path or from within PYZ archive. This is just an alias and convenience wrapper for pkgutil.get_data(). Utilizes the module_base / plugin_base as top-level reference. - :arg str fn: filename in pyz or bundle - :arg bool decode: text file decoding utf-8 - :arg bool gz: automatic gzdecode - :arg str file_base: alternative base module reference + Parameters + ---------- + filename : str + filename in pyz or bundle + decode : bool + text file decoding utf-8 + gzip : bool + automatic gzdecode + file_base : list + alternative base module reference + + Returns + ------- + str : file contents """ try: data = pkgutil.get_data(file_base or module_base, filename) if gzip: data = gzip_decode(data) @@ -184,11 +194,18 @@ def module_list(extra_paths=None): """ Search through ./plugins/ (and other configured plugin_base names → paths) and get module basenames. - :arg list extra_paths: in addition to plugin_base list + Parameters + ---------- + extra_paths : list + in addition to plugin_base list + + Returns + ------- + list : names of found plugins """ # Convert plugin_base package names into paths for iter_modules paths = [] for module_or_path in plugin_base: @@ -207,10 +224,14 @@ def all_plugin_meta(): """ This is a trivial wrapper to assemble a complete dictionary of available/installed plugins. It associates each plugin name with a its meta{} fields. + + Returns + ------- + dict : names to meta data dict """ return { name: plugin_meta(module=name) for name in module_list() } @@ -220,16 +241,28 @@ @renamed_arguments({"fn": "filename"}) def plugin_meta(filename=None, src=None, module=None, frame=1, **kwargs): """ Extract plugin meta data block from different sources: - :arg str filename: read literal files, or .pyz contents - :arg str src: from already uncovered script code - :arg str module: lookup per pkgutil, from plugin_base or top-level modules - :arg int frame: extract comment header of caller (default) - :arg list extra_base: additional search directories - :arg ist max_length: maximum size to read from files + Parameters + ---------- + filename : str + read literal files, or .pyz contents + src : str + from already uncovered script code + module : str + lookup per pkgutil, from plugin_base or top-level modules + frame : int + extract comment header of caller (default) + extra_base : list + additional search directories + max_length : list + maximum size to read from files + + Returns + ------- + dict : key-value pairs of comment fields, config: preparsed """ # Try via pkgutil first, # find any plugins.* modules, or main packages if module: @@ -276,13 +309,18 @@ """ Finds the first comment block. Splits key:value header fields from comment. Turns everything into an dict, with some stub fields if absent. - :arg str src: from existing source code - :arg int filename: set filename attribute - :arg bool literla: just split comment from doc + Parameters + ---------- + src : str + from existing source code + filename : str + set filename attribute + literls : bool + just split comment from doc """ # Defaults meta = { "id": os.path.splitext(os.path.basename(filename or ""))[0], @@ -325,16 +363,23 @@ def plugin_meta_config(src): """ Further breaks up the meta['config'] descriptor. Creates an array from JSON/YAML option lists. - :arg str src: unprocessed config: field - Stubs out name, value, type, description if absent. # config: { name: 'var1', type: text, value: "default, ..." } { name=option2, type=boolean, $value=1, etc. } + + Parameters + ---------- + src : str + unprocessed config: field + + Returns + ------- + list : of option dictionaries """ config = [] for entry in rx.config.findall(src): entry = entry[0] or entry[1] @@ -463,14 +508,20 @@ """ Utility function which collect defaults from plugin meta data to a config store. Which in the case of streamtuner2 is really just a dictionary `conf{}` and a plugin list in `conf.plugins{}`. - :arg dict conf_options: storage for amassed options - :arg dict conf_plugins: enable status based on plugin state/priority: - :arg dict meta: input plugin meta data (invoke once per plugin) - :arg str module: module name of meta: block + Parameters + ---------- + conf_options : dict : input/output + storage for amassed options + conf_plugins : dict : input/output + enable status based on plugin state/priority: + meta : dict + input plugin meta data (invoke once per plugin) + module : str + basename of meta: blocks plugin file """ # Option defaults, if not yet defined for opt in meta.get("config", []): if "name" not in opt or "value" not in opt: Index: pluginconf/depends.py ================================================================== --- pluginconf/depends.py +++ pluginconf/depends.py @@ -19,10 +19,12 @@ # Probes a new plugins` depends: list against installed base modules. # Utilizes each version: fields and allows for virtual modules, or # alternatives and honors alias: names. # +""" Dependency validation and consistency checker for updates """ + import sys import re import pluginconf try: @@ -42,11 +44,11 @@ class DependencyValidation(object): """ Now this definitely requires customization. Each plugin can carry a list of (soft-) dependency names. - # depends: config, appcore >= 2.0, bin:wkhtmltoimage, python < 3.5 + \# depends: config, appcore >= 2.0, bin:wkhtmltoimage, python < 3.5 Here only in-application modules are honored, system references ignored. Unknown plugin names are also skipped. A real install helper might want to auto-tick them on, etc. This example is just meant for probing downloadable plugins. @@ -55,10 +57,19 @@ modules, and if they're more recent. While .depends() compares minimum versions against existing modules. In practice there's little need for full-blown dependency resolving for application-level modules. + + Attributes + ---------- + api : list + allowed api: identifiers for .valid() stream checks + log : logging + warning handler + have : dict + accumulated list of existing/virtual plugins """ """ supported APIs """ api = ["python", "streamtuner2"] @@ -65,10 +76,19 @@ """ debugging """ log = logging.getLogger("pluginconf.dependency") # prepare list of known plugins and versions def __init__(self, add={}, core=["st2", "uikit", "config", "action"]): + """ + Parameters + ---------- + add : dict + name to pmd list of existing/core/virtual plugins (can define + versions or own dependencies) + core : list + name list of virtual plugins + """ self.have = { "python": {"version": sys.version} } # inject virtual modules @@ -95,10 +115,11 @@ self.have[alias] = self.have[name] # basic plugin pre-screening (skip __init__, filter by api:, # exclude installed & same-version plugins) def valid(self, new_plugin): + """ check plugin info from repository stream (fields there $name, $file, $dist, api, id, depends, etc) """ if not "$name" in new_plugin: self.log.warning(".valid() checks online plugin lists, requires $name") id = new_plugin.get("$name", "__invalid") have_ver = self.have.get(id, {}).get("version", "0") if id.find("__") == 0: @@ -112,10 +133,11 @@ else: return True # Verify depends: and breaks: against existing plugins/modules def depends(self, plugin): + """ test depends: and breaks: """ result = True if plugin.get("depends"): result &= self.and_or(self.split(plugin["depends"]), self.have) if plugin.get("breaks"): result &= self.neither(self.split(plugin["breaks"]), self.have) Index: pluginconf/flit.py ================================================================== --- pluginconf/flit.py +++ pluginconf/flit.py @@ -5,10 +5,11 @@ # version: 0.2 # depends: python:flit (>=3.0, <4.0) # license: BSD-3-Clause # priority: extra # src: ~/.local/lib/python3.8/site-packages/flit_core/ +# pylint: disable=unused-import, wrong-import-position, wrong-import-order # # This is supposed to become an alternative to pluginconf.setup, # using flit as pep517 build backend. But adding automagic field # lookup of course. # @@ -26,48 +27,55 @@ # Injecting attributes between ini reading and parameter collection # turned out easier than expanding on flit_core.buildapi functions. # And lastly, this just chains to flit.main() to handle setup and # build steps. # + +""" monkeypatches flint to use pluginconf sources for packaging """ + import sys import re import functools +import flit_core.common +import flit_core.config + import pluginconf import pluginconf.setup as psetup -import flit_core.common #-- patchy patch def inject(where): + """ monkeypatch into module """ def wrapped(func): setattr(where, func.__name__, func) return wrapped -@inject(flit_core.common) +@inject(flit_core.config) def read_flit_config(path): """ read_flit_config() with preset dynamic fields """ - d = tomllib.loads(path.read_text('utf-8')) - + ini = flit_core.config.tomli.loads(path.read_text('utf-8')) + # make fields dynamic - if not "dynamic" in d["project"]: - d["project"]["dynamic"] = [] - for dyn in ['description', 'version']: - if dyn in d["project"]: - del d["project"][dyn] - if not dyn in d["project"]["dynamic"]: - d["project"]["dynamic"].append(dyn) - print(d) + if not "dynamic" in ini["project"]: + ini["project"]["dynamic"] = [] + for dyn in ['description', 'version']: + if dyn in ini["project"]: + del ini["project"][dyn] + if not dyn in ini["project"]["dynamic"]: + ini["project"]["dynamic"].append(dyn) + print(ini) # turn it into LoadedConfig - return prep_toml_config(d, path) + return flit_core.config.prep_toml_config(ini, path) # override make_metadata @inject(flit_core.common) def make_metadata(module, ini_info): + """ different order, and obviously sources """ meta = { "name": module.name, "provides": [module.name] } meta.update(ini_info.metadata) @@ -75,59 +83,63 @@ pmd_meta( pluginconf.plugin_meta(filename=module.file), ini_info ) ) + if not meta.get("version"): + meta.update( + flit_core.common.get_info_from_module(module.file, ['version']) + ) return flit_core.common.Metadata(meta) # map plugin meta to flit Metadata def pmd_meta(pmd, ini): """ enjoin PMD fields with flit meta data """ - meta = dict( - summary = pmd.get("description"), - version = pmd.get("version"), - home_page = pmd.get("url"), - author = pmd.get("author"), # should split this into mail and name - author_email = None, - maintainer = None, - maintainer_email = None, - license = pmd.get("license"), - keywords = psetup._keywords(pmd), - download_url = None, - requires_python = psetup._python_requires(pmd).get("python_requires", ">= 2.7"), - platform = pmd.get("architecture"), - supported_platform = (), - classifiers = list(psetup._classifiers(pmd)) - + psetup._trove_license(pmd) - + psetup._trove_status(pmd), - provides = (), - requires = psetup._install_requires(pmd).get("install_requires") or (), - obsoletes = (), - project_urls = [f"{k}, {v}" for k,v in psetup._project_urls(pmd).items()], - provides_dist = (), - requires_dist = psetup._install_requires(pmd).get("install_requires") or (), - obsoletes_dist = (), - requires_external = (), - provides_extra = (), - ) + pmd = psetup.MetaUtils(pmd) + meta = { + "summary": pmd.description, + "version": pmd.version, + "home_page": pmd.url, + "author": pmd.author, # should split this into mail and name + "author_email": None, + "maintainer": None, + "maintainer_email": None, + "license": pmd.license, # {name=…} + "keywords": pmd.get_keywords(), + "download_url": None, + "requires_python": pmd.python_requires() or ">= 2.7", + "platform": pmd.architecture, + "supported_platform": (), + "classifiers": list(pmd.classifiers()) + pmd.trove_license() + pmd.trove_status(), + "provides": (), + "requires": pmd.install_requires().get("install_requires") or (), + "obsoletes": (), + "project_urls": [f"{k}, {v}" for k, v in pmd.project_urls().items()], + "provides_dist": (), + "requires_dist": pmd.install_requires().get("install_requires") or (), + "obsoletes_dist": (), + "requires_external": (), + "provides_extra": (), + } + print(meta) # comment/readme - for docs in psetup._plugin_doc(pmd), psetup._get_readme(): + for docs in pmd.plugin_doc(), psetup.get_readme(): if docs["long_description"]: - meta.update({ - k[5:]: v for k,v in docs.items() + meta.update({ # with "long_" prefix cut off + k[5:]: v for k, v in docs.items() }) # entry_points are in ini file - for section, entries in psetup._entry_points(pmd).items(): + for section, entries in pmd.entry_points().items(): ini.entrypoints[section] = ini.entrypoints.get(section, {}) - for e in entries: + for script in entries: ini.entrypoints[section].update( - dict(re.findall("(.+)=(.+)", e)) + dict(re.findall("(.+)=(.+)", script)) ) print(ini.entrypoints) - + # strip empty entries return {k: v for k, v in meta.items() if v} Index: pluginconf/gui.py ================================================================== --- pluginconf/gui.py +++ pluginconf/gui.py @@ -17,10 +17,12 @@ # pluginconf.gui.window(jsoncfg, {}, ["plugins/*.py"]) # # Very crude, and not as many widgets as the Gtk/St2 implementation. # Supports type: str, bool, select, int, dict, text config: options. # + +""" PySimpleGUI window to populate config dict via plugin options """ import PySimpleGUI as sg import pluginconf import glob, json, os, re, textwrap Index: pluginconf/setup.py ================================================================== --- pluginconf/setup.py +++ pluginconf/setup.py @@ -1,12 +1,13 @@ # encoding: utf-8 # api: setuptools -# type: functions +##type: functions # title: setup() wrapper # description: utilizes PMD to populate some package fields # version: 0.4 # license: PD +# pylint: disable=line-too-long # # Utilizes plugin meta data to cut down setup.py incantations # to basically just: # # from pluginconf.setup import setup @@ -33,159 +34,179 @@ # # keywords: tag, tag # # classifiers: tag, trove, shortcuts # # doc-format: text/markdown # # # # Long description used in lieu of README... -# +# # A README.* will be read if present, else PMD comment used. # Classifiers and license matching is very crude, just for # the most common cases. Type:, Category: and Classifiers: # or Keywords: are also scanned for trove classifers. # +""" Simulates setuptools.setup() """ + -import os, re, glob +import os +import re +import glob +import pprint import setuptools import pluginconf -def _name_to_fn(name): +def name_to_fn(name): """ find primary entry point.py from package name """ for pfx in "", "src/", "src/"+name+"/": for sfx in ".py", "/__init__.py": if os.path.exists(pfx+name+sfx): return pfx+name+sfx + return "" -def _get_readme(): +def get_readme(): """ get README.md contents """ - for fn,mime in ("README.md", "text/markdown"), ("README.rst", "text/x-rst"), ("README.txt", "text/plain"): - if os.path.exists(fn): - with open(fn, "r") as f: + for filename, mime in ("README.md", "text/markdown"), ("README.rst", "text/x-rst"), ("README.txt", "text/plain"): + if os.path.exists(filename): + with open(filename, "r") as read: return { - "long_description": f.read(), + "long_description": read.read(), "long_description_content_type": mime, } return { "long_description": "", "long_description_content_type": "text/plain", } -def _plugin_doc(pmd): - """ use comment block """ - return { - "long_description": pmd["doc"], - "long_description_content_type": pmd.get("doc_format", "text/plain"), - } - -def _python_requires(pmd): - """ # depends: python >= 3.5 """ - deps = re.findall("python\s*\(?(>=?\s?[\d.]+)", pmd.get("depends", "")) - if deps: - return {"python_requires": deps[0]} - return {} - -def _install_requires(pmd): - """ # depends: python:module, pip:module """ - deps = re.findall("(?:python|pip):([\w\-]+)\s*(\(?[<=>\s\d.\-]+)?", pmd.get("depends", "")) - if deps: - return {"install_requires": [name+re.sub("[^<=>\d.\-]", "", ver) for name,ver in deps]} - return {} - -def _extras_require(pmd): - """ # suggest: line """ - deps = re.findall("(?:python|pip):([\w\-]+)\s*\(?\s*([>=<]+\s*[\d.\-]+)", pmd.get("suggests", "")) - if deps: - return dict(deps) - return {} - -def _project_urls(pmd, exclude=["url"]): - """ # other-url: https://... """ - urls = {} - for k,url in pmd.items(): - if type(url) is str and k not in exclude and re.match("https?://\S+", url): - urls[k.title()] = url - return urls - -def _classifiers(pmd): - """ # classifiers: / keywords: / category: """ - for field in ("api", "category", "type", "keywords", "classifiers"): - field = pmd.get(field, "") - field = re.findall("(\w{4,})", field) - rx = "|".join(field) - if not rx: - continue - for line in topic_trove: - if re.search("::[^:]*("+rx+")[^:]*$", line, re.I): - yield line - -def _trove_license(pmd): - """ license: to License :: """ - trove_licenses = { - "MITL?": "License :: OSI Approved :: MIT License", - "PD|Public Domain": "License :: Public Domain", - "ASL": "License :: OSI Approved :: Apache Software License", - "art": "License :: OSI Approved :: Artistic License", - "BSDL?": "License :: OSI Approved :: BSD License", - "CPL": "License :: OSI Approved :: Common Public License", - "AGPL.*3": "License :: OSI Approved :: GNU Affero General Public License v3", - "AGPLv*3\+": "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", - "GPL": "License :: OSI Approved :: GNU General Public License (GPL)", - "GPL.*3": "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "LGPL": "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", - "MPL": "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", - "Pyth": "License :: OSI Approved :: Python Software Foundation License" - } - for rx, trove in trove_licenses.items(): - if re.search(rx, pmd["license"], re.I): - return [trove] - return [] - -def _trove_status(pmd): - """ state: to DevStatus :: """ - trove_status = { - "pre|release|cand": "Development Status :: 2 - Pre-Alpha", - "alpha": "Development Status :: 3 - Alpha", - "beta": "Development Status :: 4 - Beta", - "stable": "Development Status :: 5 - Production/Stable", - "mature": "Development Status :: 6 - Mature" - } - for rx, trove in trove_status.items(): - state = pmd.get("state") or pmd.get("status") or "alpha" - if re.search(rx, state, re.I): - return [trove] - return [] - -def _datafiles_man(): - """ data_files= """ - for man in glob.glob("man*/*.[12345678]"): - section = man[-1] - yield ("man/man"+section, [man],) - -def _entry_points(pmd): - """ collect console-scripts: """ - params = {} - for field in ["console_scripts", "gui_scripts"]: - if not pmd.get(field): - continue - params[field] = params.get(field, []) + re.findall("(\w+[^,;\s]+=\w+[^,;\s]+)", pmd[field]) - return params - -def _keywords(pmd): - """ keywords= """ - return pmd.get("keywords") or pmd.get("category") + +class MetaUtils(dict): + """ convenience access to PMD fields """ + + def __getattr__(self, name): + """ dict into properties """ + return self.get(name, "") + + def plugin_doc(self): + """ use comment block """ + return { + "long_description": self.doc, + "long_description_content_type": self.doc_format or "text/plain" + } + + def python_requires(self): + """ depends: python >= 3.5 """ + deps = re.findall(r"python\s*\(?(>=?\s?[\d.]+)", self.get("depends", "")) + if deps: + return {"python_requires": deps[0]} + return {} + + def install_requires(self): + """ depends: python:module, pip:module """ + deps = re.findall(r"(?:python|pip):([\w\-]+)\s*(\(?[<=>\s\d.\-]+)?", self.get("depends", "")) + if deps: + return {"install_requires": [name+re.sub(r"[^<=>\d.\-]", "", ver) for name, ver in deps]} + return {} + + def extras_require(self): + """ suggest: line """ + deps = re.findall(r"(?:python|pip):([\w\-]+)\s*\(?\s*([>=<]+\s*[\d.\-]+)", self.get("suggests", "")) + if deps: + return dict(deps) + return {} + + def project_urls(self, exclude=("url", "update",)): + """ other-url: https://... """ + urls = {} + for key, url in self.items(): + if isinstance(url, str) and key not in exclude and re.match(r"https?://\S+", url): + urls[key.title()] = url + return urls + + def classifiers(self): + """ classifiers: / keywords: / category: """ + for field in ("api", "category", "type", "keywords", "classifiers"): + field = self.get(field, "") + field = re.findall(r"(\w{4,})", field) + regex = "|".join(field) + if not regex: + continue + for line in TOPIC_TROVE: + if re.search("::[^:]*("+regex+")[^:]*$", line, re.I): + yield line + + def trove_license(self): + """ license: to License :: """ + trove_licenses = { + r"MITL?": "License :: OSI Approved :: MIT License", + r"\bPD\b|CC-?0|Public ?Domain|Unlicense": "License :: Public Domain", + r"ASL": "License :: OSI Approved :: Apache Software License", + r"art": "License :: OSI Approved :: Artistic License", + r"BSDL?": "License :: OSI Approved :: BSD License", + r"CPL": "License :: OSI Approved :: Common Public License", + r"AGPL.*3": "License :: OSI Approved :: GNU Affero General Public License v3", + r"AGPLv*3\+": "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", + r"\bGPL": "License :: OSI Approved :: GNU General Public License (GPL)", + r"\bGPL.*3": "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + r"LGPL": "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", + r"MPL": "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", + r"Pyth": "License :: OSI Approved :: Python Software Foundation License" + } + for regex, trove in trove_licenses.items(): + if re.search(regex, self.license, re.I): + return [trove] + return [] + + def trove_status(self): + """ state: to DevStatus :: """ + trove_status = { + "pre|release|cand": "Development Status :: 2 - Pre-Alpha", + "alpha": "Development Status :: 3 - Alpha", + "beta": "Development Status :: 4 - Beta", + "stable": "Development Status :: 5 - Production/Stable", + "mature": "Development Status :: 6 - Mature" + } + for regex, trove in trove_status.items(): + state = self.state or self.status or "alpha" + if re.search(regex, state, re.I): + return [trove] + return [] + + @staticmethod + def datafiles_man(): + """ data_files= """ + for man in glob.glob("man*/*.[12345678]"): + section = man[-1] + yield ("man/man"+section, [man],) + + def entry_points(self): + """ collect console-scripts: """ + params = {} + for field in ["console_scripts", "gui_scripts"]: + if not self.get(field): + continue + params[field] = params.get(field, []) + re.findall(r"(\w+[^,;\s]+=\w+[^,;\s]+)", self[field]) + return params + + def get_keywords(self): + """ keywords= """ + return self.keywords or self.category or self.type def setup(debug=0, **kwargs): """ Wrapper around `setuptools.setup()` which adds some defaults and plugin meta data import, with two shortcut params: - - fn="pkg/main.py", - long_description="@README.md" + + Parameters + ---------- + fn : str + main file "pkg/main.py" + long_description : str + e.g. "README.md", else comment block used - Other setup() params work as usual. + Other setup() params work as usual, and are passed trough. Notably + entry_points= or data_files= can be used, even if they get augmented. """ # stub values stub = { "classifiers": [], @@ -197,37 +218,38 @@ #"package_data": {}, #"data_files": [], "entry_points": {}, "packages": setuptools.find_packages() } - for k,v in stub.items(): - if not k in kwargs: - kwargs[k] = v + for key, val in stub.items(): + if not key in kwargs: + kwargs[key] = val # package name if "name" not in kwargs and kwargs.get("packages"): kwargs["name"] = kwargs["packages"][0] - # read README + # read README if field empty or says `@README` if re.match("^$|^[@./]*README.{0,5}$", kwargs.get("long_description", "")): - kwargs.update(_get_readme()) + kwargs.update(get_readme()) # search name= package if no fn= given if kwargs.get("filename"): kwargs["fn"] = kwargs["filename"] del kwargs["filename"] if not kwargs.get("fn") and kwargs.get("name"): - kwargs["fn"] = _name_to_fn(kwargs["name"]) + kwargs["fn"] = name_to_fn(kwargs["name"]) - # read plugin meta data (PMD) - pmd = {} - pmd = pluginconf.plugin_meta(filename=kwargs["fn"]) - + # read plugin meta data (PMD) + pmd = MetaUtils( + pluginconf.plugin_meta(filename=kwargs["fn"]) + ) + # id: if no name= still if pmd.get("id") and not kwargs.get("name"): if pmd["id"] == "__init__": - pmd["id"] = re.findall("([\w\.\-]+)/__init__.+$", kwargs["fn"])[0] + pmd["id"] = re.findall(r"([\w\.\-]+)/__init__.+$", kwargs["fn"])[0] kwargs["name"] = pmd["id"] # cleanup if "fn" in kwargs: del kwargs["fn"] @@ -235,52 +257,51 @@ for field in "version", "description", "license", "author", "url": if field in pmd and not field in kwargs: kwargs[field] = pmd[field] # other urls: - kwargs["project_urls"].update(_project_urls(pmd)) + kwargs["project_urls"].update(pmd.project_urls()) # depends: if "depends" in pmd: - kwargs.update(_python_requires(pmd)) + kwargs.update(pmd.python_requires()) if "depends" in pmd and not kwargs["install_requires"]: - kwargs.update(_install_requires(pmd)) + kwargs.update(pmd.install_requires()) # suggests: if "suggests" in pmd and not kwargs["extras_require"]: - kwargs["extras_require"].update(_extras_require(pmd)) + kwargs["extras_require"].update(pmd.extras_require()) # doc: if not kwargs.get("long_description"): - kwargs.update(_plugin_doc(pmd)) + kwargs.update(pmd.plugin_doc()) # keywords= - if not "keywords" in kwargs: - kwargs["keywords"] = _keywords(pmd) - + if "keywords" not in kwargs: + kwargs["keywords"] = pmd.get_keywords() + # automatic inclusions - kwargs["data_files"] = kwargs.get("data_files", []) + list(_datafiles_man()) + kwargs["data_files"] = kwargs.get("data_files", []) + list(pmd.datafiles_man()) # entry points - for section, entries in _entry_points(pmd).items(): + for section, entries in pmd.entry_points().items(): kwargs["entry_points"][section] = kwargs["entry_points"].get(section, []) + entries # classifiers= # license: if pmd.get("license") and not any(re.match("License ::", l) for l in kwargs["classifiers"]): - kwargs["classifiers"].extend(_trove_license(pmd)) + kwargs["classifiers"].extend(pmd.trove_license()) # state: if pmd.get("state", pmd.get("status")) and not any(re.match("Development Status ::", l) for l in kwargs["classifiers"]): - kwargs["classifiers"].extend(_trove_status(pmd)) + kwargs["classifiers"].extend(pmd.trove_status()) # topics:: - kwargs["classifiers"].extend(list(_classifiers(pmd))) + kwargs["classifiers"].extend(list(pmd.classifiers())) # handover if debug: - import pprint pprint.pprint(kwargs) setuptools.setup(**kwargs) -topic_trove="""Topic :: Adaptive Technologies +TOPIC_TROVE = """Topic :: Adaptive Technologies Topic :: Artistic Software Topic :: Communications Topic :: Communications :: BBS Topic :: Communications :: Chat Topic :: Communications :: Chat :: ICQ @@ -533,12 +554,12 @@ Topic :: Text Processing :: Markup :: Markdown Topic :: Text Processing :: Markup :: SGML Topic :: Text Processing :: Markup :: VRML Topic :: Text Processing :: Markup :: XML Topic :: Text Processing :: Markup :: reStructuredText -Topic :: Utilities""".split("\n") +Topic :: Utilities""".split(r"\n") try: from trove_classifiers import classifiers - topic_trove = [line for line in classifiers if re.match("Topic ::", line)] + TOPIC_TROVE = [line for line in classifiers if re.match("Topic ::", line)] except: pass