#!/usr/bin/env python
from setuptools import setup
setup(
  name = 'cs.deco',
  author = 'Cameron Simpson',
  author_email = 'cs@cskk.id.au',
  version = '20220311',
  url = 'https://bitbucket.org/cameron_simpson/css/commits/all',
  description =
    'Assorted decorator functions.',
  long_description =
    ('Assorted decorator functions.\n'    
 '\n'    
 '*Latest release 20220311*:\n'    
 '@cachedmethod: change the meaning of poll_delay=None to mean "never goes '    
 'stale" as I had thought it already did.\n'    
 '\n'    
 '## Function `cached(*args, **kwargs)`\n'    
 '\n'    
 'Former name for @cachedmethod.\n'    
 '\n'    
 '## Function `cachedmethod(*da, **dkw)`\n'    
 '\n'    
 'Decorator to cache the result of an instance or class method\n'    
 'and keep a revision counter for changes.\n'    
 '\n'    
 'The cached values are stored on the instance (`self`).\n'    
 'The revision counter supports the `@revised` decorator.\n'    
 '\n'    
 'This decorator may be used in 2 modes.\n'    
 'Directly:\n'    
 '\n'    
 '    @cachedmethod\n'    
 '    def method(self, ...)\n'    
 '\n'    
 'or indirectly:\n'    
 '\n'    
 '    @cachedmethod(poll_delay=0.25)\n'    
 '    def method(self, ...)\n'    
 '\n'    
 'Optional keyword arguments:\n'    
 '* `attr_name`: the basis name for the supporting attributes.\n'    
 '  Default: the name of the method.\n'    
 '* `poll_delay`: minimum time between polls; after the first\n'    
 '  access, subsequent accesses before the `poll_delay` has elapsed\n'    
 '  will return the cached value.\n'    
 '  Default: `None`, meaning the value never becomes stale.\n'    
 '* `sig_func`: a signature function, which should be significantly\n'    
 '  cheaper than the method. If the signature is unchanged, the\n'    
 '  cached value will be returned. The signature function\n'    
 '  expects the instance (`self`) as its first parameter.\n'    
 '  Default: `None`, meaning no signature function;\n'    
 '  the first computed value will be kept and never updated.\n'    
 '* `unset_value`: the value to return before the method has been\n'    
 '  called successfully.\n'    
 '  Default: `None`.\n'    
 '\n'    
 'If the method raises an exception, this will be logged and\n'    
 'the method will return the previously cached value,\n'    
 'unless there is not yet a cached value\n'    
 'in which case the exception will be reraised.\n'    
 '\n'    
 'If the signature function raises an exception\n'    
 'then a log message is issued and the signature is considered unchanged.\n'    
 '\n'    
 'An example use of this decorator might be to keep a "live"\n'    
 'configuration data structure, parsed from a configuration\n'    
 'file which might be modified after the program starts. One\n'    
 'might provide a signature function which called `os.stat()` on\n'    
 'the file to check for changes before invoking a full read and\n'    
 'parse of the file.\n'    
 '\n'    
 '*Note*: use of this decorator requires the `cs.pfx` module.\n'    
 '\n'    
 '## Function `contextdecorator(*da, **dkw)`\n'    
 '\n'    
 'A decorator for a context manager function `cmgrfunc`\n'    
 'which turns it into a decorator for other functions.\n'    
 '\n'    
 'This supports easy implementation of "setup" and "teardown"\n'    
 'code around other functions without the tedium of defining\n'    
 'the wrapper function itself. See the examples below.\n'    
 '\n'    
 'The resulting context manager accepts an optional keyword\n'    
 'parameter `provide_context`, default `False`. If true, the\n'    
 'context returned from the context manager is provided as the\n'    
 'first argument to the call to the wrapped function.\n'    
 '\n'    
 'Note that the context manager function `cmgrfunc`\n'    
 'has _not_ yet been wrapped with `@contextmanager`,\n'    
 'that is done by `@contextdecorator`.\n'    
 '\n'    
 'This decorator supports both normal functions and generator functions.\n'    
 '\n'    
 'With a normal function the process is:\n'    
 '* call the context manager with `(func,a,kw,*da,**dkw)`,\n'    
 '  returning `ctxt`,\n'    
 '  where `da` and `dkw` are the positional and keyword parameters\n'    
 '  supplied when the decorator was defined.\n'    
 '* within the context\n'    
 '  return the value of `func(ctxt,*a,**kw)` if `provide_context` is true\n'    
 '  or the value of `func(*a,**kw)` if not (the default)\n'    
 '\n'    
 'With a generator function the process is:\n'    
 '* obtain an iterator by calling `func(*a,**kw)`\n'    
 '* for iterate over the iterator, yielding its results,\n'    
 '  by calling the context manager with `(func,a,kw,**da,**dkw)`,\n'    
 '  around each `next()`\n'    
 'Note that it is an error to provide a true value for `provide_context`\n'    
 'if the decorated function is a generator function.\n'    
 '\n'    
 'Some examples follow.\n'    
 '\n'    
 'Trace the call and return of a specific function:\n'    
 '\n'    
 '    @contextdecorator\n'    
 '    def tracecall(func, a, kw):\n'    
 '        """ Trace the call and return from some function.\n'    
 '            This can easily be adapted to purposes such as timing a\n'    
 '            function call or logging use.\n'    
 '        """\n'    
 '        print("call %s(*%r,**%r)" % (func, a, kw))\n'    
 '        try:\n'    
 '          yield\n'    
 '        except Exception as e:\n'    
 '          print("exception from %s(*%r,**%r): %s" % (func, a, kw, e))\n'    
 '          raise\n'    
 '        else:\n'    
 '          print("return from %s(*%r,**%r)" % (func, a, kw))\n'    
 '\n'    
 '    @tracecall\n'    
 '    def f():\n'    
 '        """ Some function to trace.\n'    
 '        """\n'    
 '\n'    
 '    @tracecall(provide_context=True):\n'    
 '    def f(ctxt, *a, **kw):\n'    
 '        """ A function expecting the context object as its first argument,\n'    
 '            ahead of whatever other arguments it would normally require.\n'    
 '        """\n'    
 '\n'    
 "See who is making use of a generator's values,\n"    
 'when a generator might be invoked in one place and consumed elsewhere:\n'    
 '\n'    
 '    from cs.py.stack import caller\n'    
 '\n'    
 '    @contextdecorator\n'    
 '    def genuser(genfunc, *a, **kw):\n'    
 '        user = caller(-4)\n'    
 '        print(f"iterate over {genfunc}(*{a!r},**{kw!r}) from {user}")\n'    
 '        yield\n'    
 '\n'    
 '    @genuser\n'    
 '    def linesof(filename):\n'    
 '        with open(filename) as f:\n'    
 '            yield from f\n'    
 '\n'    
 '    # obtain a generator of lines here\n'    
 '    lines = linesof(__file__)\n'    
 '\n'    
 '    # perhaps much later, or in another function\n'    
 '    for lineno, line in enumerate(lines, 1):\n'    
 '        print("line %d: %d words" % (lineno, len(line.split())))\n'    
 '\n'    
 'Turn on "verbose mode" around a particular function:\n'    
 '\n'    
 '    import sys\n'    
 '    import threading\n'    
 '    from cs.context import stackattrs\n'    
 '\n'    
 '    class State(threading.local):\n'    
 '        def __init__(self):\n'    
 '            # verbose if stderr is on a terminal\n'    
 '            self.verbose = sys.stderr.isatty()\n'    
 '\n'    
 '    # per thread global state\n'    
 '    state = State()\n'    
 '\n'    
 '    @contextdecorator\n'    
 '    def verbose(func):\n'    
 '        with stackattrs(state, verbose=True) as old_attrs:\n'    
 "            if not old_attrs['verbose']:\n"    
 '                print(f"enabled verbose={state.verbose} for function '    
 '{func}")\n'    
 '            # yield the previous verbosity as the context\n'    
 "            yield old_attrs['verbose']\n"    
 '\n'    
 '    # turn on verbose mode\n'    
 '    @verbose\n'    
 '    def func(x, y):\n'    
 '        if state.verbose:\n'    
 '            # print if verbose\n'    
 '            print("x =", x, "y =", y)\n'    
 '\n'    
 '    # turn on verbose mode and also pass in the previous state\n'    
 '    # as the first argument\n'    
 '    @verbose(provide_context=True):\n'    
 '    def func2(old_verbose, x, y):\n'    
 '        if state.verbose:\n'    
 '            # print if verbose\n'    
 '            print("old_verbosity =", old_verbose, "x =", x, "y =", y)\n'    
 '\n'    
 '## Function `contextual(func)`\n'    
 '\n'    
 'Wrap a simple function as a context manager.\n'    
 '\n'    
 'This was written to support users of `@strable`,\n'    
 'which requires its `open_func` to return a context manager;\n'    
 'this turns an arbitrary function into a context manager.\n'    
 '\n'    
 'Example promoting a trivial function:\n'    
 '\n'    
 '    >>> f = lambda: 3\n'    
 '    >>> cf = contextual(f)\n'    
 '    >>> with cf() as x: print(x)\n'    
 '    3\n'    
 '\n'    
 '## Function `decorator(deco)`\n'    
 '\n'    
 'Wrapper for decorator functions to support optional arguments.\n'    
 '\n'    
 'The actual decorator function ends up being called as:\n'    
 '\n'    
 '    mydeco(func, *da, **dkw)\n'    
 '\n'    
 'allowing `da` and `dkw` to affect the behaviour of the decorator `mydeco`.\n'    
 '\n'    
 'Examples:\n'    
 '\n'    
 '    # define your decorator as if always called with func and args\n'    
 '    @decorator\n'    
 '    def mydeco(func, *da, arg2=None):\n'    
 '      ... decorate func subject to the values of da and arg2\n'    
 '\n'    
 '    # mydeco called with defaults\n'    
 '    @mydeco\n'    
 '    def func1(...):\n'    
 '      ...\n'    
 '\n'    
 '    @ mydeco called with nondefault arguments\n'    
 "    @mydeco('foo', arg2='bah')\n"    
 '    def func2(...):\n'    
 '      ...\n'    
 '\n'    
 '## Function `fmtdoc(func)`\n'    
 '\n'    
 "Decorator to replace a function's docstring with that string\n"    
 "formatted against the function's module `__dict__`.\n"    
 '\n'    
 'This supports simple formatted docstrings:\n'    
 '\n'    
 "    ENVVAR_NAME = 'FUNC_DEFAULT'\n"    
 '\n'    
 '    @fmtdoc\n'    
 '    def func():\n'    
 '        """Do something with os.environ[{ENVVAR_NAME}]."""\n'    
 '        print(os.environ[ENVVAR_NAME])\n'    
 '\n'    
 'This gives `func` this docstring:\n'    
 '\n'    
 '    Do something with os.environ[FUNC_DEFAULT].\n'    
 '\n'    
 '*Warning*: this decorator is intended for wiring "constants"\n'    
 'into docstrings, not for dynamic values. Use for other types\n'    
 'of values should be considered with trepidation.\n'    
 '\n'    
 '## Function `logging_wrapper(*da, **dkw)`\n'    
 '\n'    
 'Decorator for logging call shims\n'    
 'which bumps the `stacklevel` keyword argument so that the logging system\n'    
 'chooses the correct frame to cite in messages.\n'    
 '\n'    
 'Note: has no effect on Python < 3.8 because `stacklevel` only\n'    
 'appeared in that version.\n'    
 '\n'    
 '## Function `observable_class(property_names, only_unequal=False)`\n'    
 '\n'    
 'Class decorator to make various instance attributes observable.\n'    
 '\n'    
 'Parameters:\n'    
 '* `property_names`:\n'    
 '  an interable of instance property names to set up as\n'    
 '  observable properties. As a special case a single `str` can\n'    
 '  be supplied if only one attribute is to be observed.\n'    
 '* `only_unequal`:\n'    
 '  only call the observers if the new property value is not\n'    
 '  equal to the previous proerty value. This requires property\n'    
 '  values to be comparable for inequality.\n'    
 '  Default: `False`, meaning that all updates will be reported.\n'    
 '\n'    
 '## Function `OBSOLETE(*da, **dkw)`\n'    
 '\n'    
 'Decorator for obsolete functions.\n'    
 '\n'    
 'Use:\n'    
 '\n'    
 '    @OBSOLETE\n'    
 '    def func(...):\n'    
 '\n'    
 'This emits a warning log message before calling the decorated function.\n'    
 '\n'    
 '## Function `strable(*da, **dkw)`\n'    
 '\n'    
 'Decorator for functions which may accept a `str`\n'    
 'instead of their core type.\n'    
 '\n'    
 'Parameters:\n'    
 '* `func`: the function to decorate\n'    
 '* `open_func`: the "open" factory to produce the core type\n'    
 '  if a string is provided;\n'    
 '  the default is the builtin "open" function.\n'    
 '  The returned value should be a context manager.\n'    
 '  Simpler functions can be decorated with `@contextual`\n'    
 '  to turn them into context managers if need be.\n'    
 '\n'    
 'The usual (and default) example is a function to process an\n'    
 'open file, designed to be handed a file object but which may\n'    
 'be called with a filename. If the first argument is a `str`\n'    
 'then that file is opened and the function called with the\n'    
 'open file.\n'    
 '\n'    
 'Examples:\n'    
 '\n'    
 '    @strable\n'    
 '    def count_lines(f):\n'    
 '      return len(line for line in f)\n'    
 '\n'    
 '    class Recording:\n'    
 '      "Class representing a video recording."\n'    
 '      ...\n'    
 '    @strable(open_func=Recording)\n'    
 '    def process_video(r):\n'    
 '      ... do stuff with `r` as a Recording instance ...\n'    
 '\n'    
 '*Note*: use of this decorator requires the `cs.pfx` module.\n'    
 '\n'    
 '# Release Log\n'    
 '\n'    
 '\n'    
 '\n'    
 '*Release 20220311*:\n'    
 '@cachedmethod: change the meaning of poll_delay=None to mean "never goes '    
 'stale" as I had thought it already did.\n'    
 '\n'    
 '*Release 20220227*:\n'    
 '@cachedmethod: more paranoid access to the revision attribute.\n'    
 '\n'    
 '*Release 20210823*:\n'    
 '@decorator: preserve the __name__ of the wrapped function.\n'    
 '\n'    
 '*Release 20210123*:\n'    
 'Syntax backport for older Pythons.\n'    
 '\n'    
 '*Release 20201202*:\n'    
 '@decorator: tweak test for callable(da[0]) to accord with the docstring.\n'    
 '\n'    
 '*Release 20201025*:\n'    
 'New @contextdecorator decorator for context managers to turn them into '    
 'setup/teardown decorators.\n'    
 '\n'    
 '*Release 20201020*:\n'    
 '* @cachedmethod: bugfix cache logic.\n'    
 '* @strable: support generator functions.\n'    
 '\n'    
 '*Release 20200725*:\n'    
 'Overdue upgrade of @decorator to support combining the function and '    
 'decorator args in one call.\n'    
 '\n'    
 '*Release 20200517.2*:\n'    
 'Minor upgrade to @OBSOLETE.\n'    
 '\n'    
 '*Release 20200517.1*:\n'    
 'Tweak @OBSOLETE and @cached (obsolete name for @cachedmethod).\n'    
 '\n'    
 '*Release 20200517*:\n'    
 'Get warning() from cs.gimmicks.\n'    
 '\n'    
 '*Release 20200417*:\n'    
 '* @decorator: do not override __doc__ on the decorated function, just '    
 'provide default.\n'    
 '* New @logging_wrapper which bumps the `stacklevel` parameter in Python 3.8 '    
 'and above so that shims recite the correct caller.\n'    
 '\n'    
 '*Release 20200318.1*:\n'    
 'New @OBSOLETE to issue a warning on a call to an obsolete function, like an '    
 'improved @cs.logutils.OBSOLETE (which needs to retire).\n'    
 '\n'    
 '*Release 20200318*:\n'    
 '@cachedmethod: tighten up the "is the value changed" try/except.\n'    
 '\n'    
 '*Release 20191012*:\n'    
 '* New @contextual decorator to turn a simple function into a context '    
 'manager.\n'    
 '* @strable: mention context manager requirement and @contextual as '    
 'workaround.\n'    
 '\n'    
 '*Release 20191006*:\n'    
 'Rename @cached to @cachedmethod, leave compatible @cached behind which '    
 'issues a warning (will be removed in a future release).\n'    
 '\n'    
 '*Release 20191004*:\n'    
 'Avoid circular import with cs.pfx by removing requirement and doing the '    
 'import later if needed.\n'    
 '\n'    
 '*Release 20190905*:\n'    
 'Bugfix @deco: it turns out that you may not set the .__module__ attribute on '    
 'a property object.\n'    
 '\n'    
 '*Release 20190830.2*:\n'    
 'Make some getattr calls robust.\n'    
 '\n'    
 '*Release 20190830.1*:\n'    
 '@decorator: set the __module__ of the wrapper.\n'    
 '\n'    
 '*Release 20190830*:\n'    
 '@decorator: set the __module__ of the wrapper from the decorated target, '    
 'aids cs.distinf.\n'    
 '\n'    
 '*Release 20190729*:\n'    
 '@cached: sidestep uninitialised value.\n'    
 '\n'    
 '*Release 20190601.1*:\n'    
 '@strable: fix the example in the docstring.\n'    
 '\n'    
 '*Release 20190601*:\n'    
 '* Bugfix @decorator to correctly propagate the docstring of the '    
 'subdecorator.\n'    
 '* Improve other docstrings.\n'    
 '\n'    
 '*Release 20190526*:\n'    
 '@decorator: add support for positional arguments and rewrite - simpler and '    
 'clearer.\n'    
 '\n'    
 '*Release 20190512*:\n'    
 '@fmtdoc: add caveat against misuse of this decorator.\n'    
 '\n'    
 '*Release 20190404*:\n'    
 "New @fmtdoc decorator to format a function's doctsring against its module's "    
 'globals.\n'    
 '\n'    
 '*Release 20190403*:\n'    
 '* @cached: bugfix: avoid using unset sig_func value on first pass.\n'    
 '* @observable_class: further tweaks.\n'    
 '\n'    
 '*Release 20190322.1*:\n'    
 '@observable_class: bugfix __init__ wrapper function.\n'    
 '\n'    
 '*Release 20190322*:\n'    
 '* New class decorator @observable_class.\n'    
 '* Bugfix import of "warning".\n'    
 '\n'    
 '*Release 20190309*:\n'    
 '@cached: improve the exception handling.\n'    
 '\n'    
 '*Release 20190307.2*:\n'    
 'Fix docstring typo.\n'    
 '\n'    
 '*Release 20190307.1*:\n'    
 'Bugfix @decorator: final plumbing step for decorated decorator.\n'    
 '\n'    
 '*Release 20190307*:\n'    
 '* @decorator: drop unused arguments, they get used by the returned '    
 'decorator.\n'    
 '* Rework the @cached logic.\n'    
 '\n'    
 '*Release 20190220*:\n'    
 '* Bugfix @decorator decorator, do not decorate twice.\n'    
 "* Have a cut at inheriting the decorated function's docstring.\n"    
 '\n'    
 '*Release 20181227*:\n'    
 '* New decoartor @strable for function which may accept a str instead of '    
 'their primary type.\n'    
 '* Improvements to @cached.\n'    
 '\n'    
 '*Release 20171231*:\n'    
 'Initial PyPI release.'),
  install_requires = ['cs.gimmicks'],
  classifiers = ['Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Operating System :: OS Independent', 'Topic :: Software Development :: Libraries :: Python Modules', 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)'],
  keywords = ['python2', 'python3'],
  license = 'GNU General Public License v3 or later (GPLv3+)',
  long_description_content_type = 'text/markdown',
  package_dir = {'': 'lib/python'},
  py_modules = ['cs.deco'],
)
