Skip to content

Commit ed3f909

Browse files
committed
Merge branch 'master' into silence
2 parents cf9516e + 92b8a38 commit ed3f909

File tree

11 files changed

+263
-20
lines changed

11 files changed

+263
-20
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
## 1.4.0 (TBD, 2020)
1+
## 1.3.10 (September 17, 2020)
22
* Enhancements
33
* Added user-settable option called `always_show_hint`. If True, then tab completion hints will always
44
display even when tab completion suggestions print. Arguments whose help or hint text is suppressed will
55
not display hints even when this setting is True.
66
* argparse tab completion now groups flag names which run the same action. Optional flags are wrapped
77
in brackets like it is done in argparse usage text.
8+
* default category decorators are now heritable by default and will propagate the category down the
9+
class hierarchy until overridden. There's a new optional flag to set heritable to false.
810
* Added `--silent` flag to `alias/macro create`. If used, then no confirmation message will be printed
911
when aliases and macros are created or overwritten.
1012
* Added `--with_silent` flag to `alias/macro list`. Use this option when saving to a startup script

cmd2/cmd2.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
from .argparse_custom import DEFAULT_ARGUMENT_PARSER, CompletionItem
4747
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
4848
from .command_definition import CommandSet
49-
from .constants import COMMAND_FUNC_PREFIX, COMPLETER_FUNC_PREFIX, HELP_FUNC_PREFIX
49+
from .constants import CLASS_ATTR_DEFAULT_HELP_CATEGORY, COMMAND_FUNC_PREFIX, COMPLETER_FUNC_PREFIX, HELP_FUNC_PREFIX
5050
from .decorators import with_argparser, as_subcommand_to
5151
from .exceptions import (
5252
CommandSetRegistrationError,
@@ -483,6 +483,8 @@ def register_command_set(self, cmdset: CommandSet) -> None:
483483
predicate=lambda meth: isinstance(meth, Callable)
484484
and hasattr(meth, '__name__') and meth.__name__.startswith(COMMAND_FUNC_PREFIX))
485485

486+
default_category = getattr(cmdset, CLASS_ATTR_DEFAULT_HELP_CATEGORY, None)
487+
486488
installed_attributes = []
487489
try:
488490
for method_name, method in methods:
@@ -505,6 +507,9 @@ def register_command_set(self, cmdset: CommandSet) -> None:
505507

506508
self._cmd_to_command_sets[command] = cmdset
507509

510+
if default_category and not hasattr(method, constants.CMD_ATTR_HELP_CATEGORY):
511+
utils.categorize(method, default_category)
512+
508513
self._installed_command_sets.append(cmdset)
509514

510515
self._register_subcommands(cmdset)

cmd2/command_definition.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""
55
from typing import Optional, Type
66

7-
from .constants import COMMAND_FUNC_PREFIX
7+
from .constants import CLASS_ATTR_DEFAULT_HELP_CATEGORY, COMMAND_FUNC_PREFIX
88
from .exceptions import CommandSetRegistrationError
99

1010
# Allows IDEs to resolve types without impacting imports at runtime, breaking circular dependency issues
@@ -17,22 +17,40 @@
1717
pass
1818

1919

20-
def with_default_category(category: str):
20+
def with_default_category(category: str, *, heritable: bool = True):
2121
"""
2222
Decorator that applies a category to all ``do_*`` command methods in a class that do not already
2323
have a category specified.
2424
25+
CommandSets that are decorated by this with `heritable` set to True (default) will set a class attribute that is
26+
inherited by all subclasses unless overridden. All commands of this CommandSet and all subclasses of this CommandSet
27+
that do not declare an explicit category will be placed in this category. Subclasses may use this decorator to
28+
override the default category.
29+
30+
If `heritable` is set to False, then only the commands declared locally to this CommandSet will be placed in the
31+
specified category. Dynamically created commands, and commands declared in sub-classes will not receive this
32+
category.
33+
2534
:param category: category to put all uncategorized commands in
35+
:param heritable: Flag whether this default category should apply to sub-classes. Defaults to True
2636
:return: decorator function
2737
"""
2838

2939
def decorate_class(cls: Type[CommandSet]):
40+
if heritable:
41+
setattr(cls, CLASS_ATTR_DEFAULT_HELP_CATEGORY, category)
42+
3043
from .constants import CMD_ATTR_HELP_CATEGORY
3144
import inspect
3245
from .decorators import with_category
46+
# get members of the class that meet the following criteria:
47+
# 1. Must be a function
48+
# 2. Must start with COMMAND_FUNC_PREFIX (do_)
49+
# 3. Must be a member of the class being decorated and not one inherited from a parent declaration
3350
methods = inspect.getmembers(
3451
cls,
35-
predicate=lambda meth: inspect.isfunction(meth) and meth.__name__.startswith(COMMAND_FUNC_PREFIX))
52+
predicate=lambda meth: inspect.isfunction(meth) and meth.__name__.startswith(COMMAND_FUNC_PREFIX)
53+
and meth in inspect.getmro(cls)[0].__dict__.values())
3654
category_decorator = with_category(category)
3755
for method in methods:
3856
if not hasattr(method[1], CMD_ATTR_HELP_CATEGORY):

cmd2/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
# The custom help category a command belongs to
4141
CMD_ATTR_HELP_CATEGORY = 'help_category'
42+
CLASS_ATTR_DEFAULT_HELP_CATEGORY = 'cmd2_default_help_category'
4243

4344
# The argparse parser for the command
4445
CMD_ATTR_ARGPARSER = 'argparser'

cmd2/utils.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
import subprocess
1212
import sys
1313
import threading
14+
1415
import unicodedata
1516
from enum import Enum
16-
from typing import Any, Callable, Dict, Iterable, List, Optional, TextIO, Union
17+
from typing import Any, Callable, Dict, IO, Iterable, List, Optional, TextIO, Type, Union
1718

1819
from . import constants
1920

@@ -470,10 +471,15 @@ def getbytes(self) -> bytes:
470471
"""Get the internal contents as bytes"""
471472
return bytes(self.buffer.byte_buf)
472473

473-
def read(self) -> str:
474+
def read(self, size: Optional[int] = -1) -> str:
474475
"""Read from the internal contents as a str and then clear them out"""
475-
result = self.getvalue()
476-
self.clear()
476+
if size is None or size == -1:
477+
result = self.getvalue()
478+
self.clear()
479+
else:
480+
result = self.buffer.byte_buf[:size].decode(encoding=self.encoding, errors=self.errors)
481+
self.buffer.byte_buf = self.buffer.byte_buf[size:]
482+
477483
return result
478484

479485
def readbytes(self) -> bytes:
@@ -668,7 +674,7 @@ def __exit__(self, *args) -> None:
668674

669675
class RedirectionSavedState:
670676
"""Created by each command to store information required to restore state after redirection"""
671-
def __init__(self, self_stdout: Union[StdSim, TextIO], sys_stdout: Union[StdSim, TextIO],
677+
def __init__(self, self_stdout: Union[StdSim, IO[str]], sys_stdout: Union[StdSim, IO[str]],
672678
pipe_proc_reader: Optional[ProcReader], saved_redirecting: bool) -> None:
673679
"""
674680
RedirectionSavedState initializer
@@ -1025,11 +1031,12 @@ def categorize(func: Union[Callable, Iterable[Callable]], category: str) -> None
10251031
10261032
:Example:
10271033
1034+
>>> import cmd2
10281035
>>> class MyApp(cmd2.Cmd):
10291036
>>> def do_echo(self, arglist):
10301037
>>> self.poutput(' '.join(arglist)
10311038
>>>
1032-
>>> utils.categorize(do_echo, "Text Processing")
1039+
>>> cmd2.utils.categorize(do_echo, "Text Processing")
10331040
10341041
For an alternative approach to categorizing commands using a decorator, see
10351042
:func:`~cmd2.decorators.with_category`
@@ -1038,10 +1045,13 @@ def categorize(func: Union[Callable, Iterable[Callable]], category: str) -> None
10381045
for item in func:
10391046
setattr(item, constants.CMD_ATTR_HELP_CATEGORY, category)
10401047
else:
1041-
setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category)
1048+
if inspect.ismethod(func):
1049+
setattr(func.__func__, constants.CMD_ATTR_HELP_CATEGORY, category)
1050+
else:
1051+
setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category)
10421052

10431053

1044-
def get_defining_class(meth):
1054+
def get_defining_class(meth) -> Type:
10451055
"""
10461056
Attempts to resolve the class that defined a method.
10471057

examples/default_categories.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env python3
2+
# coding=utf-8
3+
"""
4+
Simple example demonstrating basic CommandSet usage.
5+
"""
6+
7+
import cmd2
8+
from cmd2 import CommandSet, with_default_category
9+
10+
11+
@with_default_category('Default Category')
12+
class MyBaseCommandSet(CommandSet):
13+
"""Defines a default category for all sub-class CommandSets"""
14+
pass
15+
16+
17+
class ChildInheritsParentCategories(MyBaseCommandSet):
18+
"""
19+
This subclass doesn't declare any categories so all commands here are also categorized under 'Default Category'
20+
"""
21+
def do_hello(self, _: cmd2.Statement):
22+
self._cmd.poutput('Hello')
23+
24+
def do_world(self, _: cmd2.Statement):
25+
self._cmd.poutput('World')
26+
27+
28+
@with_default_category('Non-Heritable Category', heritable=False)
29+
class ChildOverridesParentCategoriesNonHeritable(MyBaseCommandSet):
30+
"""
31+
This subclass overrides the 'Default Category' from the parent, but in a non-heritable fashion. Sub-classes of this
32+
CommandSet will not inherit this category and will, instead, inherit 'Default Category'
33+
"""
34+
def do_goodbye(self, _: cmd2.Statement):
35+
self._cmd.poutput('Goodbye')
36+
37+
38+
class GrandchildInheritsGrandparentCategory(ChildOverridesParentCategoriesNonHeritable):
39+
"""
40+
This subclass's parent class declared its default category non-heritable. Instead, it inherits the category defined
41+
by the grandparent class.
42+
"""
43+
def do_aloha(self, _: cmd2.Statement):
44+
self._cmd.poutput('Aloha')
45+
46+
47+
@with_default_category('Heritable Category')
48+
class ChildOverridesParentCategories(MyBaseCommandSet):
49+
"""
50+
This subclass is decorated with a default category that is heritable. This overrides the parent class's default
51+
category declaration.
52+
"""
53+
def do_bonjour(self, _: cmd2.Statement):
54+
self._cmd.poutput('Bonjour')
55+
56+
57+
class GrandchildInheritsHeritable(ChildOverridesParentCategories):
58+
"""
59+
This subclass's parent declares a default category that overrides its parent. As a result, commands in this
60+
CommandSet will be categorized under 'Heritable Category'
61+
"""
62+
def do_monde(self, _: cmd2.Statement):
63+
self._cmd.poutput('Monde')
64+
65+
66+
class ExampleApp(cmd2.Cmd):
67+
"""
68+
Example to demonstrate heritable default categories
69+
"""
70+
71+
def __init__(self):
72+
super(ExampleApp, self).__init__()
73+
74+
def do_something(self, arg):
75+
self.poutput('this is the something command')
76+
77+
78+
if __name__ == '__main__':
79+
app = ExampleApp()
80+
app.cmdloop()

plugins/ext_test/CHANGELOG.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7-
## 0.2.0 (TBD)
8-
- Relocated inside of cmd2 project
9-
- Fixes to match cmd2 API changes
7+
## 0.2.0 (2020-09-11)
8+
* Updated documentation to reflect new home inside of main cmd2 repo.
9+
* Updated python version requirements to match cmd2
1010

11-
## 0.1.0 (2020-03-09)
11+
## 0.1.2 (2020-08-03)
12+
* Bug Fixes
13+
* Applied fix to match change in cmd2 APIs
14+
15+
## 0.1.1 (2020-03-09)
1216

1317
### Added
1418
- Initial contribution

plugins/ext_test/setup.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,17 @@
2828

2929
author='Eric Lin',
3030
author_email='[email protected]',
31-
url='https://github.com/python-cmd2/cmd2-ext-test',
31+
url='https://github.com/python-cmd2/cmd2/tree/master/plugins/ext_test',
3232
license='MIT',
3333

3434
packages=['cmd2_ext_test'],
3535

36-
python_requires='>=3.4',
36+
python_requires='>=3.5',
3737
install_requires=['cmd2 >= 0.9.4, <=2'],
3838
setup_requires=['setuptools_scm >= 3.0'],
3939

4040
classifiers=[
41-
'Development Status :: 4 - Beta',
41+
'Development Status :: 5 - Production/Stable',
4242
'Environment :: Console',
4343
'Operating System :: OS Independent',
4444
'Topic :: Software Development :: Libraries :: Python Modules',
@@ -48,6 +48,8 @@
4848
'Programming Language :: Python :: 3.5',
4949
'Programming Language :: Python :: 3.6',
5050
'Programming Language :: Python :: 3.7',
51+
'Programming Language :: Python :: 3.8',
52+
'Programming Language :: Python :: 3.9',
5153
],
5254

5355
# dependencies for development and testing

tests/test_utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ def test_stdsim_read(stdout_sim):
154154
assert stdout_sim.read() == my_str
155155
assert stdout_sim.getvalue() == ''
156156

157+
stdout_sim.write(my_str)
158+
159+
assert stdout_sim.getvalue() == my_str
160+
assert stdout_sim.read(2) == my_str[:2]
161+
assert stdout_sim.getvalue() == my_str[2:]
162+
163+
157164
def test_stdsim_read_bytes(stdout_sim):
158165
b_str = b'Hello World'
159166
stdout_sim.buffer.write(b_str)

0 commit comments

Comments
 (0)