#!/usr/bin/env python3
# PYTHON_ARGCOMPLETE_OK
"""
Defines the subcommands for the xdev CLI.
Each subcommand is its own scriptconfig class, which is registered using a
decorator. Special "dunder" variables like ``__command__`` and ``__alias__``
are used to control subparser configurations. The normal scriptconfig
``__default__`` variable controls subparser arguments. Lastly each class must
have a ``main`` classmethod, which is the logic invoked when the subcommand is
called.
"""
import scriptconfig as scfg
import ubelt as ub
import os
import sys
from scriptconfig.modal import ModalCLI
from xdev.cli import available_package_versions
[docs]
class XdevCLI(ModalCLI):
"""
The XDEV CLI
A collection of excellent developer tools for excellent developers.
"""
[docs]
class InfoCLI(scfg.DataConfig):
"""
Info about xdev
"""
__command__ = 'info'
[docs]
@classmethod
def main(cls, cmdline=False, **kwargs):
import xdev
print('sys.version_info = {!r}'.format(sys.version_info))
print('xdev.__version__ = {!r}'.format(xdev.__version__))
print('xdev.__file__ = {!r}'.format(xdev.__file__))
[docs]
class CodeblockCLI(scfg.DataConfig):
"""
Remove indentation from text.
Useful for writing subscripts (e.g. python -c code) in shell files without
having to resort to ugly indentation.
"""
__command__ = 'codeblock'
__epilog__ = """
Example Usage
-------------
python -c "$(xdev codeblock "
import pathlib
print(list(pathlib.Path('.').glob('*')))
")"
"""
text = scfg.Value('', type=str, position=1,
help='text to remove indentation from (i.e. dedent)')
[docs]
@classmethod
def main(cls, cmdline=False, **kwargs):
"""
Example:
>>> from xdev.cli.main import * # NOQA
>>> CodeblockCLI.main(cmdline=0, text='foobar')
"""
config = cls.cli(cmdline=cmdline, data=kwargs)
print(ub.codeblock(config['text']))
[docs]
class SedCLI(scfg.DataConfig):
"""
Search and replace text in files
"""
__command__ = 'sed'
__default__ = {
'regexpr': scfg.Value('', position=1, help=ub.paragraph(
'''
The pattern to search for.
''')),
'repl': scfg.Value('', position=2, help=ub.paragraph(
'''
The pattern to replace with.
''')),
'dpath': scfg.Value(None, position=3, help=ub.paragraph(
'''
The directory to recursively search or a file pattern to match.
'''
), alias=['path']),
'dry': scfg.Value('ask', position=4, help=ub.paragraph(
'''
if 1, show what would be done. if 0, execute the change, if "ask",
then show the dry run and then ask for confirmation.
'''
)),
'include': scfg.Value(None, help='If specified, only consider results with matching basenames'),
'exclude': scfg.Value(None, help='If specified, do not consider results with matching basenames'),
'dirblocklist': scfg.Value(None, help=(
'Any directory matching this pattern will be removed from '
'traveral.')),
'recursive': scfg.Value(True),
'verbose': scfg.Value(2),
}
[docs]
@classmethod
def main(cls, cmdline=False, **kwargs):
from xdev import search_replace
config = cls.cli(cmdline=cmdline, data=kwargs)
if config['verbose'] >= 2:
rprint(f'config = {ub.urepr(config, nl=1, sort=0)}')
# print('config = {}'.format(ub.repr2(dict(config), nl=1, sort=0)))
if config['dry'] in {'ask', 'auto'}:
from rich.prompt import Confirm
config['dry'] = True
search_replace.sed(**config)
flag = Confirm.ask('Do you want to execute this sed?')
if flag:
config['dry'] = False
search_replace.sed(**config)
else:
search_replace.sed(**config)
[docs]
class FindCLI(scfg.DataConfig):
"""
Find matching files or paths in a directory.
This is similar to the GNU find program, but written in Python. Important
differences are that this program is:
* has pattern first argument and uses the cwd by default.
* recursive by default
* has explicit include / exclude options
Example
-------
xdev find "*.py"
"""
__command__ = 'find'
__default__ = {
'pattern': scfg.Value('', position=1),
'dpath': scfg.Value(None, position=2, help='the path to search. Defaults to cwd', alias=['path']),
'include': scfg.Value(None, help='If specified, only consider results with matching basenames'),
'exclude': scfg.Value(None, help='If specified, do not consider results with matching basenames'),
'dirblocklist': scfg.Value(None, help=(
'Any directory matching this pattern will be removed from '
'traveral.')),
'type': scfg.Value('f', help="can be f and/or d"),
'recursive': scfg.Value(True),
'followlinks': scfg.Value(False),
}
[docs]
@classmethod
def main(cls, cmdline=False, **kwargs):
from xdev import search_replace
config = cls.cli(cmdline=cmdline, data=kwargs)
for found in search_replace.find(**config):
print(found)
[docs]
class TreeCLI(scfg.DataConfig):
"""
List a directory like a tree
See Also
--------
The apt-installable tree command
Example
-------
xdev tree
"""
__command__ = 'tree'
__default__ = {
'cwd': scfg.Value('.', position=1),
'max_files': scfg.Value(100),
'colors': scfg.Value(not ub.NO_COLOR, isflag=True),
'dirblocklist': scfg.Value(None),
'ignore_dotprefix': scfg.Value(True, isflag=True),
'max_depth': scfg.Value(
None, help='maximum depth to recurse', short_alias=['L']),
}
[docs]
@classmethod
def main(cls, cmdline=False, **kwargs):
import xdev
config = cls.cli(cmdline=cmdline, data=kwargs)
xdev.tree_repr(**config)
# print()
[docs]
class PintCLI(scfg.DataConfig):
"""
Converts one type of unit to another via the pint library.
Notes:
See Also
--------
The pint-convert tool comes pre-installed with pint but isn't as useful for
in-bash computation unless you munge the output. The idea here is that when
something like GDAL wants an environ in bytes, we can specify it in
megabytes.
Example Usage
-------------
xdev pint "10 megabytes" "bytes" --precision=0
"""
__command__ = 'pint'
__alias__ = ['convert_unit']
__default__ = {
'input_expr': scfg.Value(None, position=1, help='A parsable pint expression with magnitude and units'),
'output_unit': scfg.Value(None, position=2, help='The output unit to convert to'),
'precision': scfg.Value(0, type=int, help='number of decimal places to use'),
}
[docs]
@classmethod
def main(cls, cmdline=False, **kwargs):
import pint
ureg = pint.UnitRegistry()
args = cls.cli(cmdline=cmdline, data=kwargs)
input = ureg.parse_expression(args['input_expr'])
output_unit = args['output_unit']
if output_unit is None:
output_unit = input.unit
output = input.to(output_unit)
if args['precision'] == 0:
print(int(output.magnitude))
else:
print(output.magnitude)
[docs]
class PyfileCLI(scfg.DataConfig):
"""
Prints the path corresponding to a Python module.
This uses the ``ubelt.modname_to_modpath`` mechanism that does not require
importing of your package.
Alternatives
------------
An alternative with no dependencies is to use the one-liner:
python -c "import <modname>; print(<modname>.__file__)"
Example Usage
-------------
xdev pyfile xdev
xdev pyfile numpy
# Use this feature in scripts for developement to avoid referencing
# machine-specific paths.
MODPATH=$(xdev pyfile ubelt)
echo "MODPATH = $MODPATH"
"""
__command__ = 'pyfile'
__alias__ = ['modpath']
# input_expr = scfg.Value(None, position=1)
# output_expr = scfg.Value(None, position=2)
__default__ = {
'modname': scfg.Value(None, position=1),
}
[docs]
@classmethod
def main(cls, cmdline=False, **kwargs):
args = cls.cli(cmdline=cmdline, data=kwargs)
modpath = ub.modname_to_modpath(args['modname'])
print(modpath)
[docs]
class PyVersionCLI(scfg.DataConfig):
"""
Detect and print the version of a Python module or package.
Note
----
Different backends may produce different results, especially for packages
that are in development and were installed in development mode.
Alternatives
------------
An alternative with no dependencies is to use the one-liner:
python -c "import <modname>; print(<modname>.__version__)"
Example Usage
-------------
xdev pyversion xdev
xdev pyversion numpy
# Both the module name and the package name can be used.
xdev pyversion cv2
xdev pyversion opencv-python-headless
xdev pyversion xdev --backend=import
xdev pyversion xdev --backend=pkg_resources
"""
__command__ = 'pyversion'
__alias__ = ['modversion']
__default__ = {
'modname': scfg.Value(None, position=1, help='The name of the module or package'),
'backend': scfg.Value('auto', help=ub.paragraph(
'''
The method to lookup the version. The core methods are 'import'
which imports the module and looks for a ``__version__`` attribute
or 'pkg_resources', which uses pip metadata. Can also be 'auto'
which tries to find the first one that works.
'''), choices=['auto', 'import', 'pkg_resources']),
}
[docs]
@classmethod
def main(cls, cmdline=False, **kwargs):
args = cls.cli(cmdline=cmdline, data=kwargs)
modname = args['modname']
if args['backend'] == 'auto':
candidate_backends = ['import', 'pkg_resources']
else:
candidate_backends = [args['backend']]
def _getversion(modname, backend):
if backend == 'import':
module = ub.import_module_from_name(modname)
version = module.__version__
elif backend == 'pkg_resources':
import pkg_resources
version = pkg_resources.get_distribution(modname).version
else:
raise KeyError(backend)
return version
version = None
for backend in candidate_backends:
try:
version = _getversion(modname, backend)
except KeyError:
raise
except Exception:
...
else:
break
print(version)
if version is None:
raise Exception(f'No version was found for {modname}')
[docs]
class EditfileCLI(scfg.DataConfig):
"""
Opens a file in your visual editor determined by the ``VISUAL``
environment variable.
If ``VISUAL`` is unspecified it attempts to default to the first known
existing editor.
Example Usage
-------------
xdev edit xdev
xdev edit numpy
"""
__command__ = 'editfile'
__alias__ = ['edit']
__default__ = {
'target': scfg.Value(None, position=1, help='a path or a module name'),
}
[docs]
@classmethod
def main(cls, cmdline=False, **kwargs):
import xdev
args = cls.cli(cmdline=cmdline, data=kwargs)
xdev.editfile(args.target)
[docs]
class FreshPyenvCLI(scfg.DataConfig):
"""
Create a fresh environment in a docker container to test a Python package.
SeeAlso
-------
The generic freshpyenv.sh bash script also installed with this package.
"""
__command__ = 'freshpyenv'
__default__ = {
'image': scfg.Value('__default__', help='The docker image to use')
}
[docs]
@classmethod
def main(cls, cmdline=False, **kwargs):
config = cls.cli(cmdline=cmdline, data=kwargs)
import ubelt as ub
ub.cmd(f'freshpyenv.sh --image={config["image"]}', system=True)
[docs]
class DocstrStubgenCLI(scfg.DataConfig):
"""
Generate Typed Stubs from Docstrings (experimental)
Note
----
This is an experimental command and currently requires a specialized patch
to mypy to work correctly.
"""
__command__ = 'docstubs'
__alias__ = ['doctypes']
__default__ = {
'module': scfg.Value(None, position=1, help=ub.paragraph(
'''
The name of a module in the PYTHONPATH or an explicit path to that
module.
''')),
}
[docs]
@classmethod
def main(cls, cmdline=False, **kwargs):
from xdev.cli import docstr_stubgen
config = cls.cli(cmdline=cmdline, data=kwargs)
print(f'config={config}')
modname_or_path = config['module']
print(f'modname_or_path={modname_or_path}')
if modname_or_path is None:
raise ValueError('Must specify the module')
modpath = docstr_stubgen.modpath_coerce(modname_or_path)
modpath = ub.Path(modpath)
generated = docstr_stubgen.generate_typed_stubs(modpath)
for fpath, text in generated.items():
fpath = ub.Path(fpath)
print(f'Write fpath={fpath}')
fpath.write_text(text)
# Generate a py.typed file to mark the package as typed
if modpath.is_dir():
pytyped_fpath = (modpath / 'py.typed')
print(f'touch pytyped_fpath={pytyped_fpath}')
pytyped_fpath.touch()
[docs]
class AvailablePackageCLI(scfg.DataConfig):
__command__ = 'available_package_versions'
__alias__ = ['availpkg']
__default__ = available_package_versions.AvailablePackageConfig.__default__
__doc__ = available_package_versions.AvailablePackageConfig.__doc__
[docs]
@classmethod
def main(cls, cmdline=False, **kwargs):
available_package_versions.main(cmdline=cmdline, **kwargs)
from xdev.cli.dirstats import DirectoryStatsCLI
[docs]
class RegexCLI(scfg.DataConfig):
"""
Query the regex builder for help on the command line.
By default prints useful regex constructs I have a hard time
remembering.
"""
__command__ = 'regex'
backend = scfg.Value('python', choices=['python', 'vim'], help='regex flavor')
[docs]
@classmethod
def main(cls, cmdline=False, **kwargs):
"""
Ignore:
from xdev.cli.main import * # NOQA
cls = XdevCLI.RegexCLI
cmdline = 0
kwargs = {}
"""
config = cls.cli(cmdline=cmdline, data=kwargs)
rprint(f'config = {ub.urepr(config, nl=1)}')
from xdev.regex_builder import RegexBuilder
b = RegexBuilder.coerce(config.backend)
rprint(f'b.constructs = {ub.urepr(b.constructs, nl=1, sk=1, align=":")}')
[docs]
def rprint(*args):
try:
import rich
rich.print(*args)
except ImportError:
print(*args)
[docs]
def main():
import xdev
cli = XdevCLI()
cli.version = xdev.__version__
XDEV_LOOSE_CLI = os.environ.get('XDEV_LOOSE_CLI', '')
cli.main(strict=not XDEV_LOOSE_CLI)
if __name__ == '__main__':
"""
CommandLine:
xdev --help
xdev --version
xdev info
xdev sed "main" "MAIN" "." --dry=True --include="*_*.py"
xdev find "*_*.py" '.'
xdev codeblock "
import sys
print(sys.argv)
print([
'hello world'
])
"
"""
main()