From 1e858172519e9339e4720b8bf9b39b6d9801e305 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sat, 28 May 2022 19:29:08 -0700 Subject: [PATCH] Tweak around docstring to make prettier sphinx autodocs. --- acl.py | 4 +- argparse_utils.py | 4 +- cached/weather_data.py | 21 +++++ config.py | 175 ++++++++++++++++++++++++++++------------- docs/conf.py | 5 +- docs/index.rst | 2 +- logging_utils.py | 4 +- parallelize.py | 4 +- persistent.py | 6 +- state_tracker.py | 13 +-- string_utils.py | 15 ++-- text_utils.py | 18 +++-- thread_utils.py | 6 +- waitable_presence.py | 2 +- 14 files changed, 185 insertions(+), 94 deletions(-) diff --git a/acl.py b/acl.py index de516e4..726dafc 100644 --- a/acl.py +++ b/acl.py @@ -183,7 +183,9 @@ class PredicateListBasedACL(SimpleACL): class StringWildcardBasedACL(PredicateListBasedACL): - """An ACL that allows or denies based on string glob (*, ?) patterns.""" + """An ACL that allows or denies based on string glob :code:`(*, ?)` + patterns. + """ def __init__( self, diff --git a/argparse_utils.py b/argparse_utils.py index 6055f1a..f73a893 100644 --- a/argparse_utils.py +++ b/argparse_utils.py @@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) class ActionNoYes(argparse.Action): - """An argparse Action that allows for commandline arguments like this: + """An argparse Action that allows for commandline arguments like this:: cfg.add_argument( '--enable_the_thing', @@ -28,7 +28,7 @@ class ActionNoYes(argparse.Action): help='Should we enable the thing?' ) - This creates cmdline arguments: + This creates the following cmdline arguments:: --enable_the_thing --no_enable_the_thing diff --git a/cached/weather_data.py b/cached/weather_data.py index 8793bd3..87c3260 100644 --- a/cached/weather_data.py +++ b/cached/weather_data.py @@ -59,6 +59,13 @@ class WeatherData: @persistent.persistent_autoloaded_singleton() # type: ignore class CachedWeatherData(persistent.Persistent): def __init__(self, weather_data: Dict[datetime.date, WeatherData] = None): + """C'tor. Do not pass a dict except for testing purposes. + + The @persistent_autoloaded_singleton decorator handles + invoking our load and save methods at construction time for + you. + """ + if weather_data is not None: self.weather_data = weather_data return @@ -186,6 +193,15 @@ class CachedWeatherData(persistent.Persistent): @classmethod @overrides def load(cls) -> Any: + + """Depending on whether we have fresh data persisted either uses that + data to instantiate the class or makes an HTTP request to fetch the + necessary data. + + Note that because this is a subclass of Persistent this is taken + care of automatically. + """ + if persistent.was_file_written_within_n_seconds( config.config['weather_data_cachefile'], config.config['weather_data_stalest_acceptable'].total_seconds(), @@ -199,6 +215,11 @@ class CachedWeatherData(persistent.Persistent): @overrides def save(self) -> bool: + """ + Saves the current data to disk if required. Again, because this is + a subclass of Persistent this is taken care of for you. + """ + import pickle with open(config.config['weather_data_cachefile'], 'wb') as wf: diff --git a/config.py b/config.py index 599026c..c5813a8 100644 --- a/config.py +++ b/config.py @@ -7,64 +7,64 @@ and saved configuration files. This works across several modules. Usage: - module.py: - ---------- - import config - - parser = config.add_commandline_args( - "Module", - "Args related to module doing the thing.", - ) - parser.add_argument( - "--module_do_the_thing", - type=bool, - default=True, - help="Should the module do the thing?" - ) - - main.py: - -------- - import config - - def main() -> None: + In your file.py:: + + import config + parser = config.add_commandline_args( - "Main", - "A program that does the thing.", + "Module", + "Args related to module doing the thing.", ) parser.add_argument( - "--dry_run", + "--module_do_the_thing", type=bool, - default=False, - help="Should we really do the thing?" + default=True, + help="Should the module do the thing?" ) - config.parse() # Very important, this must be invoked! + + In your main.py:: + + import config + + def main() -> None: + parser = config.add_commandline_args( + "Main", + "A program that does the thing.", + ) + parser.add_argument( + "--dry_run", + type=bool, + default=False, + help="Should we really do the thing?" + ) + config.parse() # Very important, this must be invoked! If you set this up and remember to invoke config.parse(), all commandline arguments will play nicely together. This is done automatically for you - if you're using the bootstrap module's initialize wrapper. + if you're using the bootstrap module's initialize wrapper.:: - % main.py -h - usage: main.py [-h] - [--module_do_the_thing MODULE_DO_THE_THING] - [--dry_run DRY_RUN] + % main.py -h + usage: main.py [-h] + [--module_do_the_thing MODULE_DO_THE_THING] + [--dry_run DRY_RUN] - Module: - Args related to module doing the thing. + Module: + Args related to module doing the thing. - --module_do_the_thing MODULE_DO_THE_THING - Should the module do the thing? + --module_do_the_thing MODULE_DO_THE_THING + Should the module do the thing? - Main: - A program that does the thing + Main: + A program that does the thing - --dry_run - Should we really do the thing? + --dry_run + Should we really do the thing? Arguments themselves should be accessed via - config.config['arg_name']. e.g. + :code:`config.config['arg_name']`. e.g.:: - if not config.config['dry_run']: - module.do_the_thing() + if not config.config['dry_run']: + module.do_the_thing() """ @@ -92,6 +92,33 @@ class OptionalRawFormatter(argparse.HelpFormatter): "RAW|". In that case, the line breaks are preserved and the text is not wrapped. + Use this, for example, when you need the helptext of an argument + to have its spacing preserved exactly, e.g.:: + + args.add_argument( + '--mode', + type=str, + default='PLAY', + choices=['CHEAT', 'AUTOPLAY', 'SELFTEST', 'PRECOMPUTE', 'PLAY'], + metavar='MODE', + help='''RAW|Our mode of operation. One of: + + PLAY = play wordle with me! Pick a random solution or + specify a solution with --template. + + CHEAT = given a --template and, optionally, --letters_in_word + and/or --letters_to_avoid, return the best guess word; + + AUTOPLAY = given a complete word in --template, guess it step + by step showing work; + + SELFTEST = autoplay every possible solution keeping track of + wins/losses and average number of guesses; + + PRECOMPUTE = populate hash table with optimal guesses. + ''', + ) + """ def _split_lines(self, text, width): @@ -127,8 +154,16 @@ config: Dict[str, Any] = {} # It would be really nice if this shit worked from interactive python -def add_commandline_args(title: str, description: str = ""): - """Create a new context for arguments and return a handle.""" +def add_commandline_args(title: str, description: str = "") -> argparse._ArgumentGroup: + """Create a new context for arguments and return a handle. + + Args: + title: A title for your module's commandline arguments group. + description: A helpful description of your module. + + Returns: + An argparse._ArgumentGroup to be populated by the caller. + """ return ARGS.add_argument_group(title, description) @@ -168,18 +203,33 @@ group.add_argument( def overwrite_argparse_epilog(msg: str) -> None: + """Allows your code to override the default epilog created by + argparse. + + Args: + msg: The epilog message to substitute for the default. + """ ARGS.epilog = msg -def is_flag_already_in_argv(var: str): - """Is a particular flag passed on the commandline?""" +def is_flag_already_in_argv(var: str) -> bool: + """Returns true if a particular flag is passed on the commandline? + + Args: + var: The flag to search for. + """ for _ in sys.argv: if var in _: return True return False -def reorder_arg_action_groups_before_help(entry_module: Optional[str]): +def _reorder_arg_action_groups_before_help(entry_module: Optional[str]): + """Internal. Used to reorder the arguments before dumping out a + generated help string such that the main program's arguments come + last. + + """ reordered_action_groups = [] for grp in ARGS._action_groups: if entry_module is not None and entry_module in grp.title: # type: ignore @@ -191,7 +241,21 @@ def reorder_arg_action_groups_before_help(entry_module: Optional[str]): return reordered_action_groups -def augment_sys_argv_from_environment_variables(): +def _augment_sys_argv_from_environment_variables(): + """Internal. Look at the system environment for variables that match + arg names. This is done via some munging such that: + + :code:`--argument_to_match` + + ...is matched by: + + :code:`ARGUMENT_TO_MATCH` + + This allows programmers to set args via shell environment variables + in lieu of passing them on the cmdline. + + """ + usage_message = ARGS.format_usage() optional = False var = '' @@ -227,7 +291,9 @@ def augment_sys_argv_from_environment_variables(): env = '' -def augment_sys_argv_from_loadfile(): +def _augment_sys_argv_from_loadfile(): + """Internal. Augment with arguments persisted in a saved file.""" + loadfile = None saw_other_args = False grab_next_arg = False @@ -263,7 +329,8 @@ def augment_sys_argv_from_loadfile(): def parse(entry_module: Optional[str]) -> Dict[str, Any]: """Main program should call this early in main(). Note that the - bootstrap.initialize wrapper takes care of this automatically. + :code:`bootstrap.initialize` wrapper takes care of this automatically. + This should only be called once per program invocation. """ global CONFIG_PARSE_CALLED @@ -278,17 +345,17 @@ def parse(entry_module: Optional[str]) -> Dict[str, Any]: if arg in ('--help', '-h'): if entry_module is not None: entry_module = os.path.basename(entry_module) - ARGS._action_groups = reorder_arg_action_groups_before_help(entry_module) + ARGS._action_groups = _reorder_arg_action_groups_before_help(entry_module) # Examine the environment for variables that match known flags. # For a flag called --example_flag the corresponding environment # variable would be called EXAMPLE_FLAG. If found, hackily add # these into sys.argv to be parsed. - augment_sys_argv_from_environment_variables() + _augment_sys_argv_from_environment_variables() # Look for loadfile and read/parse it if present. This also # works by jamming these values onto sys.argv. - augment_sys_argv_from_loadfile() + _augment_sys_argv_from_loadfile() # Parse (possibly augmented, possibly completely overwritten) # commandline args with argparse normally and populate config. @@ -322,7 +389,7 @@ def parse(entry_module: Optional[str]) -> Dict[str, Any]: def has_been_parsed() -> bool: - """Has the global config been parsed yet?""" + """Returns True iff the global config has already been parsed""" return CONFIG_PARSE_CALLED diff --git a/docs/conf.py b/docs/conf.py index 2d1ed09..ef2a272 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,11 +38,14 @@ author = 'Scott Gasch' # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.doctest', 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.napoleon', 'sphinx.ext.viewcode', ] +autodoc_typehints = "both" + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/index.rst b/docs/index.rst index 6e6da4d..3d9731e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,7 +7,7 @@ Welcome to Scott's Python Utils's documentation! ================================================ .. toctree:: - :maxdepth: 2 + :maxdepth: 3 :caption: Contents: modules diff --git a/logging_utils.py b/logging_utils.py index 6ceba65..78785ba 100644 --- a/logging_utils.py +++ b/logging_utils.py @@ -669,7 +669,6 @@ class OutputMultiplexer(object): various logging levels, different files, different file handles, the house log, etc...). See also OutputMultiplexerContext for an easy usage pattern. - """ class Destination(enum.IntEnum): @@ -784,7 +783,7 @@ class OutputMultiplexer(object): class OutputMultiplexerContext(OutputMultiplexer, contextlib.ContextDecorator): """ - A context that uses an OutputMultiplexer. e.g. + A context that uses an OutputMultiplexer. e.g.:: with OutputMultiplexerContext( OutputMultiplexer.LOG_INFO | @@ -795,7 +794,6 @@ class OutputMultiplexerContext(OutputMultiplexer, contextlib.ContextDecorator): handles = [ f, g ] ) as mplex: mplex.print("This is a log message!") - """ def __init__( diff --git a/parallelize.py b/parallelize.py index 77d7649..6005d42 100644 --- a/parallelize.py +++ b/parallelize.py @@ -22,7 +22,7 @@ class Method(Enum): def parallelize( _funct: typing.Optional[typing.Callable] = None, *, method: Method = Method.THREAD ) -> typing.Callable: - """Usage: + """Usage:: @parallelize # defaults to thread-mode def my_function(a, b, c) -> int: @@ -36,7 +36,7 @@ def parallelize( def my_other_other_function(g, h) -> int: ...this work will be distributed to a remote machine pool - This decorator will invoke the wrapped function on: + This decorator will invoke the wrapped function on:: Method.THREAD (default): a background thread Method.PROCESS: a background process diff --git a/persistent.py b/persistent.py index b42a5c0..0391144 100644 --- a/persistent.py +++ b/persistent.py @@ -32,7 +32,6 @@ class Persistent(ABC): """ Save this thing somewhere that you'll remember when someone calls load() later on in a way that makes sense to your code. - """ pass @@ -45,7 +44,7 @@ class Persistent(ABC): below) be save()d at program exit time. Oh, in case this is handy, here's how to write a factory - method that doesn't call the c'tor in python: + method that doesn't call the c'tor in python:: @classmethod def load_from_somewhere(cls, somewhere): @@ -58,7 +57,6 @@ class Persistent(ABC): # Load the piece(s) of obj that you want to from somewhere. obj._state = load_from_somewhere(somewhere) return obj - """ pass @@ -127,7 +125,6 @@ class PersistAtShutdown(enum.Enum): """ An enum to describe the conditions under which state is persisted to disk. See details below. - """ NEVER = (0,) @@ -153,7 +150,6 @@ class persistent_autoloaded_singleton(object): The implementations of save() and load() and where the class persists its state are details left to the Persistent implementation. - """ def __init__( diff --git a/state_tracker.py b/state_tracker.py index 62eb183..66d2de6 100644 --- a/state_tracker.py +++ b/state_tracker.py @@ -6,7 +6,6 @@ polling. StateTracker expects to be invoked periodically to maintain state whereas the others automatically update themselves and, optionally, expose an event for client code to wait on state changes. - """ import datetime @@ -29,7 +28,6 @@ class StateTracker(ABC): invoked via the heartbeat() method. This method, in turn, invokes update() with update_ids according to a schedule / periodicity provided to the c'tor. - """ def __init__(self, update_ids_to_update_secs: Dict[str, float]) -> None: @@ -40,7 +38,6 @@ class StateTracker(ABC): Note that, when more than one update is overdue, they will be invoked in order by their update_ids so care in choosing these identifiers may be in order. - """ self.update_ids_to_update_secs = update_ids_to_update_secs self.last_reminder_ts: Dict[str, Optional[datetime.datetime]] = {} @@ -61,7 +58,6 @@ class StateTracker(ABC): The now param is the approximate current timestamp and the last_invocation param is the last time you were invoked (or None on the first invocation) - """ pass @@ -77,8 +73,8 @@ class StateTracker(ABC): Setting force_all_updates_to_run will invoke all updates (ordered by update_id) immediately ignoring whether or not they are due. - """ + self.now = datetime.datetime.now(tz=pytz.timezone("US/Pacific")) for update_id in sorted(self.last_reminder_ts.keys()): if force_all_updates_to_run: @@ -109,7 +105,6 @@ class AutomaticStateTracker(StateTracker): """Just like HeartbeatCurrentState but you don't need to pump the heartbeat; it runs on a background thread. Call .shutdown() to terminate the updates. - """ @background_thread @@ -117,7 +112,6 @@ class AutomaticStateTracker(StateTracker): """Entry point for a background thread to own calling heartbeat() at regular intervals so that the main thread doesn't need to do so. - """ while True: if should_terminate.is_set(): @@ -150,8 +144,8 @@ class AutomaticStateTracker(StateTracker): def shutdown(self): """Terminates the background thread and waits for it to tear down. This may block for as long as self.sleep_delay. - """ + logger.debug('Setting shutdown event and waiting for background thread.') self.should_terminate.set() self.updater_thread.join() @@ -166,7 +160,7 @@ class WaitableAutomaticStateTracker(AutomaticStateTracker): simply timed out. If the return value is true, the instance should be reset() before wait is called again. - Example usage: + Example usage:: detector = waitable_presence.WaitableAutomaticStateSubclass() while True: @@ -177,7 +171,6 @@ class WaitableAutomaticStateTracker(AutomaticStateTracker): else: # Just a timeout; no need to reset. Maybe do something # else before looping up into wait again. - """ def __init__( diff --git a/string_utils.py b/string_utils.py index a2f4633..dbd50f2 100644 --- a/string_utils.py +++ b/string_utils.py @@ -1246,13 +1246,18 @@ def strip_ansi_sequences(in_str: str) -> str: class SprintfStdout(contextlib.AbstractContextManager): """ - A context manager that captures outputs to stdout. + A context manager that captures outputs to stdout to a buffer + without printing them. e.g.:: - with SprintfStdout() as buf: - print("test") - print(buf()) + with SprintfStdout() as buf: + print("test") + print("1, 2, 3") + print(buf()) + + This yields:: + + 'test\\n1, 2, 3\\n' - 'test\n' """ def __init__(self) -> None: diff --git a/text_utils.py b/text_utils.py index 46f3756..28ab755 100644 --- a/text_utils.py +++ b/text_utils.py @@ -307,12 +307,20 @@ def wrap_string(text: str, n: int) -> str: class Indenter(contextlib.AbstractContextManager): """ - with Indenter(pad_count = 8) as i: - i.print('test') - with i: - i.print('-ing') + Context manager that indents stuff (even recursively). e.g.:: + + with Indenter(pad_count = 8) as i: + i.print('test') with i: - i.print('1, 2, 3') + i.print('-ing') + with i: + i.print('1, 2, 3') + + Yields:: + + test + -ing + 1, 2, 3 """ diff --git a/thread_utils.py b/thread_utils.py index 01755de..5903782 100644 --- a/thread_utils.py +++ b/thread_utils.py @@ -72,7 +72,7 @@ def background_thread( *** event as an input parameter and should periodically check *** *** it and stop if the event is set. *** - Usage: + Usage:: @background_thread def random(a: int, b: str, stop_event: threading.Event) -> None: @@ -82,7 +82,6 @@ def background_thread( if stop_event.is_set(): return - def main() -> None: (thread, event) = random(22, "dude") print("back!") @@ -131,13 +130,12 @@ def periodically_invoke( Returns a Thread object and an Event that, when signaled, will stop the invocations. Note that it is possible to be invoked one time after the Event is set. This event can be used to stop infinite - invocation style or finite invocation style decorations. + invocation style or finite invocation style decorations.:: @periodically_invoke(period_sec=0.5, stop_after=None) def there(name: str, age: int) -> None: print(f" ...there {name}, {age}") - @periodically_invoke(period_sec=1.0, stop_after=3) def hello(name: str) -> None: print(f"Hello, {name}") diff --git a/waitable_presence.py b/waitable_presence.py index 6473add..4bd5d6c 100644 --- a/waitable_presence.py +++ b/waitable_presence.py @@ -26,7 +26,7 @@ class WaitablePresenceDetectorWithMemory(state_tracker.WaitableAutomaticStateTra """ This is a waitable class that keeps a PresenceDetector internally and periodically polls it to detect changes in presence in a - particular location. Example suggested usage pattern: + particular location. Example suggested usage pattern:: detector = waitable_presence.WaitablePresenceDetectorWithMemory(60.0) while True: -- 2.45.0