devshell
Advanced tools
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" /> | ||
| <meta name="generator" content="pdoc 0.9.2" /> | ||
| <title>devshell API documentation</title> | ||
| <meta name="description" content="" /> | ||
| <link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin> | ||
| <link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/typography.min.css" integrity="sha256-7l/o7C8jubJiy74VsKTidCy1yBkRtiUGbVkYBylBqUg=" crossorigin> | ||
| <link rel="stylesheet preload" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/github.min.css" crossorigin> | ||
| <style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style> | ||
| <style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style> | ||
| <style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style> | ||
| <script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha256-Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8=" crossorigin></script> | ||
| <script>window.addEventListener('DOMContentLoaded', () => hljs.initHighlighting())</script> | ||
| </head> | ||
| <body> | ||
| <main> | ||
| <article id="content"> | ||
| <header> | ||
| <h1 class="title">Package <code>devshell</code></h1> | ||
| </header> | ||
| <section id="section-intro"> | ||
| <details class="source"> | ||
| <summary> | ||
| <span>Expand source code</span> | ||
| </summary> | ||
| <pre><code class="python">__version__ = '0.0.3' | ||
| from .injector import doctestify, set_end_interactive | ||
| from .shell import DevshellCmd</code></pre> | ||
| </details> | ||
| </section> | ||
| <section> | ||
| <h2 class="section-title" id="header-submodules">Sub-modules</h2> | ||
| <dl> | ||
| <dt><code class="name"><a title="devshell.injector" href="injector.html">devshell.injector</a></code></dt> | ||
| <dd> | ||
| <div class="desc"></div> | ||
| </dd> | ||
| <dt><code class="name"><a title="devshell.ptcmd" href="ptcmd.html">devshell.ptcmd</a></code></dt> | ||
| <dd> | ||
| <div class="desc"><p>This script takes the Cmd object from the built-in standard library, adds an input_method attribute, and replaces all calls to the standard input() …</p></div> | ||
| </dd> | ||
| <dt><code class="name"><a title="devshell.shell" href="shell.html">devshell.shell</a></code></dt> | ||
| <dd> | ||
| <div class="desc"></div> | ||
| </dd> | ||
| </dl> | ||
| </section> | ||
| <section> | ||
| </section> | ||
| <section> | ||
| </section> | ||
| <section> | ||
| </section> | ||
| </article> | ||
| <nav id="sidebar"> | ||
| <h1>Index</h1> | ||
| <div class="toc"> | ||
| <ul></ul> | ||
| </div> | ||
| <ul id="index"> | ||
| <li><h3><a href="#header-submodules">Sub-modules</a></h3> | ||
| <ul> | ||
| <li><code><a title="devshell.injector" href="injector.html">devshell.injector</a></code></li> | ||
| <li><code><a title="devshell.ptcmd" href="ptcmd.html">devshell.ptcmd</a></code></li> | ||
| <li><code><a title="devshell.shell" href="shell.html">devshell.shell</a></code></li> | ||
| </ul> | ||
| </li> | ||
| </ul> | ||
| </nav> | ||
| </main> | ||
| <footer id="footer"> | ||
| <p>Generated by <a href="https://pdoc3.github.io/pdoc"><cite>pdoc</cite> 0.9.2</a>.</p> | ||
| </footer> | ||
| </body> | ||
| </html> |
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" /> | ||
| <meta name="generator" content="pdoc 0.9.2" /> | ||
| <title>devshell.injector API documentation</title> | ||
| <meta name="description" content="" /> | ||
| <link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin> | ||
| <link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/typography.min.css" integrity="sha256-7l/o7C8jubJiy74VsKTidCy1yBkRtiUGbVkYBylBqUg=" crossorigin> | ||
| <link rel="stylesheet preload" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/github.min.css" crossorigin> | ||
| <style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style> | ||
| <style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style> | ||
| <style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style> | ||
| <script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha256-Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8=" crossorigin></script> | ||
| <script>window.addEventListener('DOMContentLoaded', () => hljs.initHighlighting())</script> | ||
| </head> | ||
| <body> | ||
| <main> | ||
| <article id="content"> | ||
| <header> | ||
| <h1 class="title">Module <code>devshell.injector</code></h1> | ||
| </header> | ||
| <section id="section-intro"> | ||
| <details class="source"> | ||
| <summary> | ||
| <span>Expand source code</span> | ||
| </summary> | ||
| <pre><code class="python">import inspect, ast, re, sys, code, readline, importlib, os, doctest, os.path | ||
| from io import StringIO | ||
| try: | ||
| from importlib import reload | ||
| except: | ||
| pass | ||
| try: | ||
| input = raw_input | ||
| except NameError: | ||
| pass | ||
| def get_target(target_fqn): | ||
| """ | ||
| This function returns the target object, module object, and the module's fully qualified name based on the provided fully qualified target name. | ||
| """ | ||
| module_fqn = target_fqn.split('.') | ||
| while True: | ||
| try: | ||
| module = __import__('.'.join(module_fqn)) | ||
| break | ||
| except ImportError: | ||
| module_fqn.pop() | ||
| if len(module_fqn) == 0: | ||
| raise Exception('Could not resolve target: %s' % repr(target_fqn)) | ||
| pieces = target_fqn.split('.') | ||
| obj = module | ||
| for item in pieces[1:]: | ||
| obj = getattr(obj,item) | ||
| return obj,module,'.'.join(module_fqn) | ||
| get_target.__annotations__ = {'target_fqn':'fully qualified name of target','return':('target object','top-level target module','fully qualified name of module')} | ||
| class _ModStdout(object): | ||
| def __init__(self,iobuf): | ||
| self.iobuf = iobuf | ||
| def flush(self): | ||
| sys.__stdout__.flush() | ||
| def writelines(self,lines): | ||
| self.iobuf.extend(data) | ||
| sys.__stdout__.writelines(lines) | ||
| def write(self,data): | ||
| self.iobuf.append(data) | ||
| sys.__stdout__.write(data) | ||
| class _ModStderr(object): | ||
| def __init__(self,iobuf): | ||
| self.iobuf = iobuf | ||
| def flush(self): | ||
| sys.__stderr__.flush() | ||
| def writelines(self,lines): | ||
| self.iobuf.extend(data) | ||
| sys.__stderr__.writelines(lines) | ||
| def write(self,data): | ||
| self.iobuf.append(data) | ||
| sys.__stderr__.write(data) | ||
| def get_ast_obj(target_fqn,obj=None,module=None,module_fqn=None): | ||
| """ | ||
| This function returns the ast object of the targeted python object | ||
| """ | ||
| if obj is None or module is None or module_fqn is None: | ||
| obj,module,module_fqn = get_target(target_fqn) | ||
| importlib.reload(module) | ||
| if inspect.ismodule(obj): | ||
| importlib.reload(sys.modules[obj.__name__]) | ||
| else: | ||
| importlib.reload(sys.modules[obj.__module__]) | ||
| filepath = os.path.abspath(inspect.getsourcefile(obj)) | ||
| if not filepath.startswith(os.getcwd()): | ||
| raise Exception('Referenced file is not in the current working directory or any subfolders - this is to protect you from modifying system or site-package code: %s' % repr(filepath)) | ||
| filepath | ||
| pieces = target_fqn.split('.') | ||
| with open(filepath,'r') as f: | ||
| src_lines = f.readlines() | ||
| source = ''.join(src_lines) | ||
| tree = ast.parse(source) | ||
| if inspect.ismodule(obj): | ||
| ast_obj = tree | ||
| elif inspect.isclass(obj): | ||
| ast_obj = [node for node in ast.walk(tree) if isinstance(node,ast.ClassDef) and node.name == pieces[-1]][0] | ||
| elif inspect.isfunction(obj): | ||
| ast_obj = [node for node in ast.walk(tree) if isinstance(node,ast.FunctionDef) and node.name == pieces[-1]][0] | ||
| return ast_obj, filepath,source, src_lines | ||
| class DoctestInjector(object): | ||
| """ | ||
| This class loads a target object by its fully qualified name and parses its source code to determine how to insert docstring lines for that object. | ||
| """ | ||
| def __init__(self,target_fqn): | ||
| self.target_fqn = target_fqn | ||
| obj,module,module_fqn = get_target(target_fqn) | ||
| self.obj = obj | ||
| self.module=module | ||
| self.module_fqn = module_fqn | ||
| ast_obj,self.filepath,self.original_source,self.src_lines = get_ast_obj(target_fqn,obj,module,module_fqn) | ||
| if isinstance(ast_obj.body[0],ast.Expr) and isinstance(ast_obj.body[0].value,ast.Str): | ||
| #docstring already exists | ||
| ast_doc = ast_obj.body[0] | ||
| if hasattr(ast_doc,'end_lineno'): | ||
| #python 3.8+ | ||
| line_index = ast_doc.end_lineno-1 #last line of docstring (line containing the ending quotes) | ||
| else: | ||
| #python 3.7- | ||
| line_index = ast_doc.lineno-1 #last line of docstring (line containing the ending quotes) | ||
| byte_index = ast_doc.col_offset | ||
| indentation = re.search('^\\s*',self.src_lines[line_index]).group(0) #use docstring end line to determine indentation | ||
| newline = re.search('[\r\n]+$',self.src_lines[line_index]).group(0) #use docstring end line to determine newline | ||
| top = self.src_lines[:line_index+1] #include entire docstring including end line | ||
| bottom = self.src_lines[line_index+1:] #everything else | ||
| ending = '"""'+top[-1].split('"""')[-1] #get the trailing quotes and characters after doctsring | ||
| top[-1] = top[-1][len(indentation):-len(ending)]+newline #remove trailing from top | ||
| bottom.insert(0,indentation+ending) #add trailing to bottom | ||
| else: | ||
| if len(ast_obj.body) == 1 and ast_obj.lineno == ast_obj.body[0].lineno: | ||
| #docstring does not exist for a single-line function | ||
| line_index = ast_obj.lineno-1 #line of function | ||
| indentation = re.search('^\\s*',self.src_lines[line_index]).group(0).strip('\r\n')+' ' #use indentation of function plus four spaces | ||
| newline = re.search('[\r\n]+$',self.src_lines[line_index]).group(0) #use newline of function line | ||
| top = self.src_lines[:line_index+1] #include funtion | ||
| bottom = src_lines[ast_obj.lineno:] #everything after function | ||
| ast_first = ast_obj.body[0] #first (and only) element in body | ||
| byte_index = ast_first.col_offset #starting position of first element | ||
| first_element = top[-1][byte_index:] #first element text content | ||
| top[-1] = top[-1][:byte_index]+newline #remove first element text content from top | ||
| bottom.insert(0,indentation+first_element) #add first element text content to bottom | ||
| top.append(indentation+'"""'+newline) #add docstring starting quotes | ||
| bottom.insert(0,indentation+'"""'+newline) #add docstring ending quotes | ||
| else: | ||
| #docstring does not exist for a multi-line function | ||
| ast_first = ast_obj.body[0] | ||
| line_index = ast_first.lineno-1 #line number of first element in body of definition | ||
| byte_index = ast_first.col_offset | ||
| indentation = re.search('^\\s*',self.src_lines[line_index]).group(0) #use first element line to determine indentation | ||
| newline = re.search('[\r\n]+$',self.src_lines[line_index]).group(0) #use first element line to determine newline | ||
| top = self.src_lines[:line_index] #everything up to but not including first element | ||
| top.append(indentation+'"""'+newline) #add new docstring starting quotes | ||
| bottom = self.src_lines[ast_obj.body[0].lineno-1:] #first element and everything after | ||
| bottom.insert(0,indentation+'"""'+newline) #add docstring ending quotes | ||
| self.top = top | ||
| self.bottom = bottom | ||
| self.indentation = indentation | ||
| self.newline = newline | ||
| self.middle = [] | ||
| __init__.__annotations__ = {'target_fqn':'fully qualified name of target','return':('target object','target module','fully qualified name of module')} | ||
| def source(self): | ||
| """ | ||
| This returns the updated source code with new inserted docstrings lines for the target object. | ||
| """ | ||
| indented_middle = [] | ||
| last_line = None | ||
| for line in self.middle: | ||
| line = re.sub(self.newline,self.newline+self.indentation,line) | ||
| if last_line is None: | ||
| indented_middle.append(self.indentation+line) | ||
| else: | ||
| indented_middle.append(line) | ||
| last_line = line | ||
| indented_middle[-1] = indented_middle[-1].rstrip() + self.newline #don't indent the ending triple quotes | ||
| return ''.join(self.top+ indented_middle + self.bottom) | ||
| def doctest_console(self,resume=False): | ||
| """ | ||
| This function runs doctests on the target file, loads the file, and enters a special interactive mode with inputs/outputs being recorded. | ||
| When the console is done being used (via Ctrl+D), the recorded inputs/outputs will be inserted into the docstring of the target object. | ||
| Doctests are then run for the udpated code and if there are no problems, the updated code is written to the file location. | ||
| If there are problems, the updated code is saved to a file in the same folder as the target file but with the suffix ".failed_doctest_insert". | ||
| """ | ||
| print('='*80+'\nDoctestify:\n Testing doctest execution of original file') | ||
| oldfailcount,oldtestcount = self.testmod() | ||
| print(' ...done: Fail count = %d, Total count = %d' % (oldfailcount,oldtestcount)) | ||
| print('='*80+'''\nEntering interactive console: | ||
| Target: %s | ||
| File: %s | ||
| (*) Press Ctrl+D to exit and incorporate session into the targeted docstring | ||
| (*) To abort without incorporating anything, call the exit() function | ||
| ''' % (self.target_fqn,self.filepath) + '-'*80) | ||
| console = code.InteractiveConsole() | ||
| print('>>> from %s import * # automatic import by devshell''' % (self.module_fqn)) | ||
| console.push('from %s import *' % (self.module_fqn)) | ||
| if resume: | ||
| doc = inspect.getdoc(self.obj) | ||
| for line in doc.splitlines(): | ||
| line = line.lstrip() | ||
| if line.startswith('>>> ') or line.startswith('... '): | ||
| print(line) | ||
| console.push(line[4:]) | ||
| iobuf = self.middle | ||
| _modstdout = _ModStdout(iobuf) | ||
| _modstderr = _ModStderr(iobuf) | ||
| def mod_input(prompt,iobuf=iobuf,_modstdout=_modstdout,_modstderr=_modstderr,newline=self.newline): | ||
| sys.stdout = sys.__stdout__ #when input() is happening - need normal stdout for readline (up/down arrowkeys) to work properly | ||
| sys.stderr = sys.__stderr__ | ||
| s = input(prompt) | ||
| iobuf.append(prompt+s+newline) | ||
| sys.stdout = _modstdout | ||
| sys.stderr = _modstderr | ||
| return s | ||
| console.raw_input = mod_input | ||
| #sys.stdout = _modstdout | ||
| #sys.stderr = _modstderr | ||
| console.interact(banner='') | ||
| sys.stdout = sys.__stdout__ | ||
| sys.stderr = sys.__stderr__ | ||
| if len(iobuf) == 0: | ||
| print('No lines were written - exiting') | ||
| else: | ||
| print('Writing doctest lines to file') | ||
| updated_source = self.source() | ||
| with open(self.filepath,'w') as f: | ||
| f.write(updated_source) | ||
| print('Testing doctest execution of new file') | ||
| revert = False | ||
| try: | ||
| newfailcount,newtestcount = self.testmod() | ||
| print('...done: Fail count = %d (old=%d), Total count = %d (old=%d)' % (newfailcount,oldfailcount,newtestcount,oldtestcount)) | ||
| except: | ||
| revert = True | ||
| print('Failed to load new file - reverting back to original file') | ||
| if revert is False and oldfailcount != newfailcount: | ||
| revert = True | ||
| print('Failcounts from before did not match after - reverting back to original file') | ||
| if revert: | ||
| with open(self.filepath,'w') as f: | ||
| f.write(self.original_source) | ||
| print('Updated source code with problems located at: %s' % (self.filepath+'.failed_doctest_insert')) | ||
| with open(self.filepath+'.failed_doctest_insert','w') as f: | ||
| f.write(updated_source) | ||
| else: | ||
| print('File successfully updated') | ||
| def testmod(self): | ||
| """ | ||
| This runs doctests on the target module and returns the failcount and testcount | ||
| """ | ||
| self.module = module = reload(sys.modules[self.module_fqn]) | ||
| failcount,testcount = doctest.testmod(module) | ||
| return failcount,testcount | ||
| def set_end_interactive(value=True): | ||
| """ | ||
| Setting value=True will make python go into interactive mode when the script terminates. | ||
| """ | ||
| if value: | ||
| os.environ['PYTHONINSPECT'] = '1' | ||
| else: | ||
| if 'PYTHONINSPECT' in os.environ: | ||
| del os.environ['PYTHONINSPECT'] | ||
| def doctestify(target_fqn,resume=False): | ||
| """ | ||
| Start an interactive recording session for the item identified by the given fully qualified name. | ||
| Write the recorded results to the target object's docstring and test that the doctest passes. | ||
| """ | ||
| di = DoctestInjector(target_fqn) | ||
| di.doctest_console(resume)</code></pre> | ||
| </details> | ||
| </section> | ||
| <section> | ||
| </section> | ||
| <section> | ||
| </section> | ||
| <section> | ||
| <h2 class="section-title" id="header-functions">Functions</h2> | ||
| <dl> | ||
| <dt id="devshell.injector.doctestify"><code class="name flex"> | ||
| <span>def <span class="ident">doctestify</span></span>(<span>target_fqn, resume=False)</span> | ||
| </code></dt> | ||
| <dd> | ||
| <div class="desc"><p>Start an interactive recording session for the item identified by the given fully qualified name. | ||
| Write the recorded results to the target object's docstring and test that the doctest passes.</p></div> | ||
| <details class="source"> | ||
| <summary> | ||
| <span>Expand source code</span> | ||
| </summary> | ||
| <pre><code class="python">def doctestify(target_fqn,resume=False): | ||
| """ | ||
| Start an interactive recording session for the item identified by the given fully qualified name. | ||
| Write the recorded results to the target object's docstring and test that the doctest passes. | ||
| """ | ||
| di = DoctestInjector(target_fqn) | ||
| di.doctest_console(resume)</code></pre> | ||
| </details> | ||
| </dd> | ||
| <dt id="devshell.injector.get_ast_obj"><code class="name flex"> | ||
| <span>def <span class="ident">get_ast_obj</span></span>(<span>target_fqn, obj=None, module=None, module_fqn=None)</span> | ||
| </code></dt> | ||
| <dd> | ||
| <div class="desc"><p>This function returns the ast object of the targeted python object</p></div> | ||
| <details class="source"> | ||
| <summary> | ||
| <span>Expand source code</span> | ||
| </summary> | ||
| <pre><code class="python">def get_ast_obj(target_fqn,obj=None,module=None,module_fqn=None): | ||
| """ | ||
| This function returns the ast object of the targeted python object | ||
| """ | ||
| if obj is None or module is None or module_fqn is None: | ||
| obj,module,module_fqn = get_target(target_fqn) | ||
| importlib.reload(module) | ||
| if inspect.ismodule(obj): | ||
| importlib.reload(sys.modules[obj.__name__]) | ||
| else: | ||
| importlib.reload(sys.modules[obj.__module__]) | ||
| filepath = os.path.abspath(inspect.getsourcefile(obj)) | ||
| if not filepath.startswith(os.getcwd()): | ||
| raise Exception('Referenced file is not in the current working directory or any subfolders - this is to protect you from modifying system or site-package code: %s' % repr(filepath)) | ||
| filepath | ||
| pieces = target_fqn.split('.') | ||
| with open(filepath,'r') as f: | ||
| src_lines = f.readlines() | ||
| source = ''.join(src_lines) | ||
| tree = ast.parse(source) | ||
| if inspect.ismodule(obj): | ||
| ast_obj = tree | ||
| elif inspect.isclass(obj): | ||
| ast_obj = [node for node in ast.walk(tree) if isinstance(node,ast.ClassDef) and node.name == pieces[-1]][0] | ||
| elif inspect.isfunction(obj): | ||
| ast_obj = [node for node in ast.walk(tree) if isinstance(node,ast.FunctionDef) and node.name == pieces[-1]][0] | ||
| return ast_obj, filepath,source, src_lines</code></pre> | ||
| </details> | ||
| </dd> | ||
| <dt id="devshell.injector.get_target"><code class="name flex"> | ||
| <span>def <span class="ident">get_target</span></span>(<span>target_fqn: fully qualified name of target) ‑> ('target object', 'top-level target module', 'fully qualified name of module')</span> | ||
| </code></dt> | ||
| <dd> | ||
| <div class="desc"><p>This function returns the target object, module object, and the module's fully qualified name based on the provided fully qualified target name.</p></div> | ||
| <details class="source"> | ||
| <summary> | ||
| <span>Expand source code</span> | ||
| </summary> | ||
| <pre><code class="python">def get_target(target_fqn): | ||
| """ | ||
| This function returns the target object, module object, and the module's fully qualified name based on the provided fully qualified target name. | ||
| """ | ||
| module_fqn = target_fqn.split('.') | ||
| while True: | ||
| try: | ||
| module = __import__('.'.join(module_fqn)) | ||
| break | ||
| except ImportError: | ||
| module_fqn.pop() | ||
| if len(module_fqn) == 0: | ||
| raise Exception('Could not resolve target: %s' % repr(target_fqn)) | ||
| pieces = target_fqn.split('.') | ||
| obj = module | ||
| for item in pieces[1:]: | ||
| obj = getattr(obj,item) | ||
| return obj,module,'.'.join(module_fqn)</code></pre> | ||
| </details> | ||
| </dd> | ||
| <dt id="devshell.injector.set_end_interactive"><code class="name flex"> | ||
| <span>def <span class="ident">set_end_interactive</span></span>(<span>value=True)</span> | ||
| </code></dt> | ||
| <dd> | ||
| <div class="desc"><p>Setting value=True will make python go into interactive mode when the script terminates.</p></div> | ||
| <details class="source"> | ||
| <summary> | ||
| <span>Expand source code</span> | ||
| </summary> | ||
| <pre><code class="python">def set_end_interactive(value=True): | ||
| """ | ||
| Setting value=True will make python go into interactive mode when the script terminates. | ||
| """ | ||
| if value: | ||
| os.environ['PYTHONINSPECT'] = '1' | ||
| else: | ||
| if 'PYTHONINSPECT' in os.environ: | ||
| del os.environ['PYTHONINSPECT']</code></pre> | ||
| </details> | ||
| </dd> | ||
| </dl> | ||
| </section> | ||
| <section> | ||
| <h2 class="section-title" id="header-classes">Classes</h2> | ||
| <dl> | ||
| <dt id="devshell.injector.DoctestInjector"><code class="flex name class"> | ||
| <span>class <span class="ident">DoctestInjector</span></span> | ||
| <span>(</span><span>target_fqn: fully qualified name of target)</span> | ||
| </code></dt> | ||
| <dd> | ||
| <div class="desc"><p>This class loads a target object by its fully qualified name and parses its source code to determine how to insert docstring lines for that object.</p></div> | ||
| <details class="source"> | ||
| <summary> | ||
| <span>Expand source code</span> | ||
| </summary> | ||
| <pre><code class="python">class DoctestInjector(object): | ||
| """ | ||
| This class loads a target object by its fully qualified name and parses its source code to determine how to insert docstring lines for that object. | ||
| """ | ||
| def __init__(self,target_fqn): | ||
| self.target_fqn = target_fqn | ||
| obj,module,module_fqn = get_target(target_fqn) | ||
| self.obj = obj | ||
| self.module=module | ||
| self.module_fqn = module_fqn | ||
| ast_obj,self.filepath,self.original_source,self.src_lines = get_ast_obj(target_fqn,obj,module,module_fqn) | ||
| if isinstance(ast_obj.body[0],ast.Expr) and isinstance(ast_obj.body[0].value,ast.Str): | ||
| #docstring already exists | ||
| ast_doc = ast_obj.body[0] | ||
| if hasattr(ast_doc,'end_lineno'): | ||
| #python 3.8+ | ||
| line_index = ast_doc.end_lineno-1 #last line of docstring (line containing the ending quotes) | ||
| else: | ||
| #python 3.7- | ||
| line_index = ast_doc.lineno-1 #last line of docstring (line containing the ending quotes) | ||
| byte_index = ast_doc.col_offset | ||
| indentation = re.search('^\\s*',self.src_lines[line_index]).group(0) #use docstring end line to determine indentation | ||
| newline = re.search('[\r\n]+$',self.src_lines[line_index]).group(0) #use docstring end line to determine newline | ||
| top = self.src_lines[:line_index+1] #include entire docstring including end line | ||
| bottom = self.src_lines[line_index+1:] #everything else | ||
| ending = '"""'+top[-1].split('"""')[-1] #get the trailing quotes and characters after doctsring | ||
| top[-1] = top[-1][len(indentation):-len(ending)]+newline #remove trailing from top | ||
| bottom.insert(0,indentation+ending) #add trailing to bottom | ||
| else: | ||
| if len(ast_obj.body) == 1 and ast_obj.lineno == ast_obj.body[0].lineno: | ||
| #docstring does not exist for a single-line function | ||
| line_index = ast_obj.lineno-1 #line of function | ||
| indentation = re.search('^\\s*',self.src_lines[line_index]).group(0).strip('\r\n')+' ' #use indentation of function plus four spaces | ||
| newline = re.search('[\r\n]+$',self.src_lines[line_index]).group(0) #use newline of function line | ||
| top = self.src_lines[:line_index+1] #include funtion | ||
| bottom = src_lines[ast_obj.lineno:] #everything after function | ||
| ast_first = ast_obj.body[0] #first (and only) element in body | ||
| byte_index = ast_first.col_offset #starting position of first element | ||
| first_element = top[-1][byte_index:] #first element text content | ||
| top[-1] = top[-1][:byte_index]+newline #remove first element text content from top | ||
| bottom.insert(0,indentation+first_element) #add first element text content to bottom | ||
| top.append(indentation+'"""'+newline) #add docstring starting quotes | ||
| bottom.insert(0,indentation+'"""'+newline) #add docstring ending quotes | ||
| else: | ||
| #docstring does not exist for a multi-line function | ||
| ast_first = ast_obj.body[0] | ||
| line_index = ast_first.lineno-1 #line number of first element in body of definition | ||
| byte_index = ast_first.col_offset | ||
| indentation = re.search('^\\s*',self.src_lines[line_index]).group(0) #use first element line to determine indentation | ||
| newline = re.search('[\r\n]+$',self.src_lines[line_index]).group(0) #use first element line to determine newline | ||
| top = self.src_lines[:line_index] #everything up to but not including first element | ||
| top.append(indentation+'"""'+newline) #add new docstring starting quotes | ||
| bottom = self.src_lines[ast_obj.body[0].lineno-1:] #first element and everything after | ||
| bottom.insert(0,indentation+'"""'+newline) #add docstring ending quotes | ||
| self.top = top | ||
| self.bottom = bottom | ||
| self.indentation = indentation | ||
| self.newline = newline | ||
| self.middle = [] | ||
| __init__.__annotations__ = {'target_fqn':'fully qualified name of target','return':('target object','target module','fully qualified name of module')} | ||
| def source(self): | ||
| """ | ||
| This returns the updated source code with new inserted docstrings lines for the target object. | ||
| """ | ||
| indented_middle = [] | ||
| last_line = None | ||
| for line in self.middle: | ||
| line = re.sub(self.newline,self.newline+self.indentation,line) | ||
| if last_line is None: | ||
| indented_middle.append(self.indentation+line) | ||
| else: | ||
| indented_middle.append(line) | ||
| last_line = line | ||
| indented_middle[-1] = indented_middle[-1].rstrip() + self.newline #don't indent the ending triple quotes | ||
| return ''.join(self.top+ indented_middle + self.bottom) | ||
| def doctest_console(self,resume=False): | ||
| """ | ||
| This function runs doctests on the target file, loads the file, and enters a special interactive mode with inputs/outputs being recorded. | ||
| When the console is done being used (via Ctrl+D), the recorded inputs/outputs will be inserted into the docstring of the target object. | ||
| Doctests are then run for the udpated code and if there are no problems, the updated code is written to the file location. | ||
| If there are problems, the updated code is saved to a file in the same folder as the target file but with the suffix ".failed_doctest_insert". | ||
| """ | ||
| print('='*80+'\nDoctestify:\n Testing doctest execution of original file') | ||
| oldfailcount,oldtestcount = self.testmod() | ||
| print(' ...done: Fail count = %d, Total count = %d' % (oldfailcount,oldtestcount)) | ||
| print('='*80+'''\nEntering interactive console: | ||
| Target: %s | ||
| File: %s | ||
| (*) Press Ctrl+D to exit and incorporate session into the targeted docstring | ||
| (*) To abort without incorporating anything, call the exit() function | ||
| ''' % (self.target_fqn,self.filepath) + '-'*80) | ||
| console = code.InteractiveConsole() | ||
| print('>>> from %s import * # automatic import by devshell''' % (self.module_fqn)) | ||
| console.push('from %s import *' % (self.module_fqn)) | ||
| if resume: | ||
| doc = inspect.getdoc(self.obj) | ||
| for line in doc.splitlines(): | ||
| line = line.lstrip() | ||
| if line.startswith('>>> ') or line.startswith('... '): | ||
| print(line) | ||
| console.push(line[4:]) | ||
| iobuf = self.middle | ||
| _modstdout = _ModStdout(iobuf) | ||
| _modstderr = _ModStderr(iobuf) | ||
| def mod_input(prompt,iobuf=iobuf,_modstdout=_modstdout,_modstderr=_modstderr,newline=self.newline): | ||
| sys.stdout = sys.__stdout__ #when input() is happening - need normal stdout for readline (up/down arrowkeys) to work properly | ||
| sys.stderr = sys.__stderr__ | ||
| s = input(prompt) | ||
| iobuf.append(prompt+s+newline) | ||
| sys.stdout = _modstdout | ||
| sys.stderr = _modstderr | ||
| return s | ||
| console.raw_input = mod_input | ||
| #sys.stdout = _modstdout | ||
| #sys.stderr = _modstderr | ||
| console.interact(banner='') | ||
| sys.stdout = sys.__stdout__ | ||
| sys.stderr = sys.__stderr__ | ||
| if len(iobuf) == 0: | ||
| print('No lines were written - exiting') | ||
| else: | ||
| print('Writing doctest lines to file') | ||
| updated_source = self.source() | ||
| with open(self.filepath,'w') as f: | ||
| f.write(updated_source) | ||
| print('Testing doctest execution of new file') | ||
| revert = False | ||
| try: | ||
| newfailcount,newtestcount = self.testmod() | ||
| print('...done: Fail count = %d (old=%d), Total count = %d (old=%d)' % (newfailcount,oldfailcount,newtestcount,oldtestcount)) | ||
| except: | ||
| revert = True | ||
| print('Failed to load new file - reverting back to original file') | ||
| if revert is False and oldfailcount != newfailcount: | ||
| revert = True | ||
| print('Failcounts from before did not match after - reverting back to original file') | ||
| if revert: | ||
| with open(self.filepath,'w') as f: | ||
| f.write(self.original_source) | ||
| print('Updated source code with problems located at: %s' % (self.filepath+'.failed_doctest_insert')) | ||
| with open(self.filepath+'.failed_doctest_insert','w') as f: | ||
| f.write(updated_source) | ||
| else: | ||
| print('File successfully updated') | ||
| def testmod(self): | ||
| """ | ||
| This runs doctests on the target module and returns the failcount and testcount | ||
| """ | ||
| self.module = module = reload(sys.modules[self.module_fqn]) | ||
| failcount,testcount = doctest.testmod(module) | ||
| return failcount,testcount</code></pre> | ||
| </details> | ||
| <h3>Methods</h3> | ||
| <dl> | ||
| <dt id="devshell.injector.DoctestInjector.doctest_console"><code class="name flex"> | ||
| <span>def <span class="ident">doctest_console</span></span>(<span>self, resume=False)</span> | ||
| </code></dt> | ||
| <dd> | ||
| <div class="desc"><p>This function runs doctests on the target file, loads the file, and enters a special interactive mode with inputs/outputs being recorded. | ||
| When the console is done being used (via Ctrl+D), the recorded inputs/outputs will be inserted into the docstring of the target object. | ||
| Doctests are then run for the udpated code and if there are no problems, the updated code is written to the file location. | ||
| If there are problems, the updated code is saved to a file in the same folder as the target file but with the suffix ".failed_doctest_insert".</p></div> | ||
| <details class="source"> | ||
| <summary> | ||
| <span>Expand source code</span> | ||
| </summary> | ||
| <pre><code class="python"> def doctest_console(self,resume=False): | ||
| """ | ||
| This function runs doctests on the target file, loads the file, and enters a special interactive mode with inputs/outputs being recorded. | ||
| When the console is done being used (via Ctrl+D), the recorded inputs/outputs will be inserted into the docstring of the target object. | ||
| Doctests are then run for the udpated code and if there are no problems, the updated code is written to the file location. | ||
| If there are problems, the updated code is saved to a file in the same folder as the target file but with the suffix ".failed_doctest_insert". | ||
| """ | ||
| print('='*80+'\nDoctestify:\n Testing doctest execution of original file') | ||
| oldfailcount,oldtestcount = self.testmod() | ||
| print(' ...done: Fail count = %d, Total count = %d' % (oldfailcount,oldtestcount)) | ||
| print('='*80+'''\nEntering interactive console: | ||
| Target: %s | ||
| File: %s | ||
| (*) Press Ctrl+D to exit and incorporate session into the targeted docstring | ||
| (*) To abort without incorporating anything, call the exit() function | ||
| ''' % (self.target_fqn,self.filepath) + '-'*80) | ||
| console = code.InteractiveConsole() | ||
| print('>>> from %s import * # automatic import by devshell''' % (self.module_fqn)) | ||
| console.push('from %s import *' % (self.module_fqn)) | ||
| if resume: | ||
| doc = inspect.getdoc(self.obj) | ||
| for line in doc.splitlines(): | ||
| line = line.lstrip() | ||
| if line.startswith('>>> ') or line.startswith('... '): | ||
| print(line) | ||
| console.push(line[4:]) | ||
| iobuf = self.middle | ||
| _modstdout = _ModStdout(iobuf) | ||
| _modstderr = _ModStderr(iobuf) | ||
| def mod_input(prompt,iobuf=iobuf,_modstdout=_modstdout,_modstderr=_modstderr,newline=self.newline): | ||
| sys.stdout = sys.__stdout__ #when input() is happening - need normal stdout for readline (up/down arrowkeys) to work properly | ||
| sys.stderr = sys.__stderr__ | ||
| s = input(prompt) | ||
| iobuf.append(prompt+s+newline) | ||
| sys.stdout = _modstdout | ||
| sys.stderr = _modstderr | ||
| return s | ||
| console.raw_input = mod_input | ||
| #sys.stdout = _modstdout | ||
| #sys.stderr = _modstderr | ||
| console.interact(banner='') | ||
| sys.stdout = sys.__stdout__ | ||
| sys.stderr = sys.__stderr__ | ||
| if len(iobuf) == 0: | ||
| print('No lines were written - exiting') | ||
| else: | ||
| print('Writing doctest lines to file') | ||
| updated_source = self.source() | ||
| with open(self.filepath,'w') as f: | ||
| f.write(updated_source) | ||
| print('Testing doctest execution of new file') | ||
| revert = False | ||
| try: | ||
| newfailcount,newtestcount = self.testmod() | ||
| print('...done: Fail count = %d (old=%d), Total count = %d (old=%d)' % (newfailcount,oldfailcount,newtestcount,oldtestcount)) | ||
| except: | ||
| revert = True | ||
| print('Failed to load new file - reverting back to original file') | ||
| if revert is False and oldfailcount != newfailcount: | ||
| revert = True | ||
| print('Failcounts from before did not match after - reverting back to original file') | ||
| if revert: | ||
| with open(self.filepath,'w') as f: | ||
| f.write(self.original_source) | ||
| print('Updated source code with problems located at: %s' % (self.filepath+'.failed_doctest_insert')) | ||
| with open(self.filepath+'.failed_doctest_insert','w') as f: | ||
| f.write(updated_source) | ||
| else: | ||
| print('File successfully updated')</code></pre> | ||
| </details> | ||
| </dd> | ||
| <dt id="devshell.injector.DoctestInjector.source"><code class="name flex"> | ||
| <span>def <span class="ident">source</span></span>(<span>self)</span> | ||
| </code></dt> | ||
| <dd> | ||
| <div class="desc"><p>This returns the updated source code with new inserted docstrings lines for the target object.</p></div> | ||
| <details class="source"> | ||
| <summary> | ||
| <span>Expand source code</span> | ||
| </summary> | ||
| <pre><code class="python">def source(self): | ||
| """ | ||
| This returns the updated source code with new inserted docstrings lines for the target object. | ||
| """ | ||
| indented_middle = [] | ||
| last_line = None | ||
| for line in self.middle: | ||
| line = re.sub(self.newline,self.newline+self.indentation,line) | ||
| if last_line is None: | ||
| indented_middle.append(self.indentation+line) | ||
| else: | ||
| indented_middle.append(line) | ||
| last_line = line | ||
| indented_middle[-1] = indented_middle[-1].rstrip() + self.newline #don't indent the ending triple quotes | ||
| return ''.join(self.top+ indented_middle + self.bottom)</code></pre> | ||
| </details> | ||
| </dd> | ||
| <dt id="devshell.injector.DoctestInjector.testmod"><code class="name flex"> | ||
| <span>def <span class="ident">testmod</span></span>(<span>self)</span> | ||
| </code></dt> | ||
| <dd> | ||
| <div class="desc"><p>This runs doctests on the target module and returns the failcount and testcount</p></div> | ||
| <details class="source"> | ||
| <summary> | ||
| <span>Expand source code</span> | ||
| </summary> | ||
| <pre><code class="python">def testmod(self): | ||
| """ | ||
| This runs doctests on the target module and returns the failcount and testcount | ||
| """ | ||
| self.module = module = reload(sys.modules[self.module_fqn]) | ||
| failcount,testcount = doctest.testmod(module) | ||
| return failcount,testcount</code></pre> | ||
| </details> | ||
| </dd> | ||
| </dl> | ||
| </dd> | ||
| </dl> | ||
| </section> | ||
| </article> | ||
| <nav id="sidebar"> | ||
| <h1>Index</h1> | ||
| <div class="toc"> | ||
| <ul></ul> | ||
| </div> | ||
| <ul id="index"> | ||
| <li><h3>Super-module</h3> | ||
| <ul> | ||
| <li><code><a title="devshell" href="index.html">devshell</a></code></li> | ||
| </ul> | ||
| </li> | ||
| <li><h3><a href="#header-functions">Functions</a></h3> | ||
| <ul class=""> | ||
| <li><code><a title="devshell.injector.doctestify" href="#devshell.injector.doctestify">doctestify</a></code></li> | ||
| <li><code><a title="devshell.injector.get_ast_obj" href="#devshell.injector.get_ast_obj">get_ast_obj</a></code></li> | ||
| <li><code><a title="devshell.injector.get_target" href="#devshell.injector.get_target">get_target</a></code></li> | ||
| <li><code><a title="devshell.injector.set_end_interactive" href="#devshell.injector.set_end_interactive">set_end_interactive</a></code></li> | ||
| </ul> | ||
| </li> | ||
| <li><h3><a href="#header-classes">Classes</a></h3> | ||
| <ul> | ||
| <li> | ||
| <h4><code><a title="devshell.injector.DoctestInjector" href="#devshell.injector.DoctestInjector">DoctestInjector</a></code></h4> | ||
| <ul class=""> | ||
| <li><code><a title="devshell.injector.DoctestInjector.doctest_console" href="#devshell.injector.DoctestInjector.doctest_console">doctest_console</a></code></li> | ||
| <li><code><a title="devshell.injector.DoctestInjector.source" href="#devshell.injector.DoctestInjector.source">source</a></code></li> | ||
| <li><code><a title="devshell.injector.DoctestInjector.testmod" href="#devshell.injector.DoctestInjector.testmod">testmod</a></code></li> | ||
| </ul> | ||
| </li> | ||
| </ul> | ||
| </li> | ||
| </ul> | ||
| </nav> | ||
| </main> | ||
| <footer id="footer"> | ||
| <p>Generated by <a href="https://pdoc3.github.io/pdoc"><cite>pdoc</cite> 0.9.2</a>.</p> | ||
| </footer> | ||
| </body> | ||
| </html> |
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" /> | ||
| <meta name="generator" content="pdoc 0.9.2" /> | ||
| <title>devshell.ptcmd API documentation</title> | ||
| <meta name="description" content="This script takes the Cmd object from the built-in standard library, adds an input_method attribute, and replaces all calls to the standard input() …" /> | ||
| <link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin> | ||
| <link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/typography.min.css" integrity="sha256-7l/o7C8jubJiy74VsKTidCy1yBkRtiUGbVkYBylBqUg=" crossorigin> | ||
| <link rel="stylesheet preload" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/github.min.css" crossorigin> | ||
| <style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style> | ||
| <style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style> | ||
| <style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style> | ||
| <script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha256-Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8=" crossorigin></script> | ||
| <script>window.addEventListener('DOMContentLoaded', () => hljs.initHighlighting())</script> | ||
| </head> | ||
| <body> | ||
| <main> | ||
| <article id="content"> | ||
| <header> | ||
| <h1 class="title">Module <code>devshell.ptcmd</code></h1> | ||
| </header> | ||
| <section id="section-intro"> | ||
| <p>This script takes the Cmd object from the built-in standard library, adds an input_method attribute, and replaces all calls to the standard input() with calls to the input_method attribute</p> | ||
| <details class="source"> | ||
| <summary> | ||
| <span>Expand source code</span> | ||
| </summary> | ||
| <pre><code class="python">""" | ||
| This script takes the Cmd object from the built-in standard library, adds an input_method attribute, and replaces all calls to the standard input() with calls to the input_method attribute | ||
| """ | ||
| import ast, inspect, cmd, shlex | ||
| from prompt_toolkit.completion import Completer, Completion | ||
| from prompt_toolkit import PromptSession | ||
| class PTCmd_Completer(Completer): | ||
| def __init__(self,ptcmd): | ||
| self.ptcmd = ptcmd | ||
| def get_completions(self,document,complete_event): | ||
| for suggestion in self.ptcmd.pt_complete(document,complete_event): | ||
| #yield Completion(suggestion,start_position=0) | ||
| try: | ||
| yield Completion(suggestion,-len(shlex.split(document.current_line_before_cursor)[-1])) | ||
| except: | ||
| pass | ||
| class PTCmd(cmd.Cmd): | ||
| def __init__(self, completekey='tab', stdin=None, stdout=None,**psession_kwargs): | ||
| super().__init__(completekey,stdin,stdout) | ||
| psession_kwargs['completer'] = PTCmd_Completer(self) | ||
| psession_kwargs['complete_while_typing'] = True | ||
| self.psession = PromptSession(**psession_kwargs) | ||
| self.input_method = self.psession.prompt | ||
| def pt_complete(self, document,complete_event): | ||
| origline = document.text | ||
| line = origline.lstrip() | ||
| stripped = len(origline) - len(line) | ||
| begidx = document.cursor_position_col - stripped | ||
| endidx = len(document.text) - stripped | ||
| if begidx>0: | ||
| cmd, args, foo = self.parseline(line) | ||
| if cmd == '': | ||
| compfunc = self.completedefault | ||
| else: | ||
| try: | ||
| compfunc = getattr(self, 'complete_' + cmd) | ||
| except AttributeError: | ||
| compfunc = self.completedefault | ||
| else: | ||
| compfunc = self.completenames | ||
| self.completion_matches = compfunc(document.text, line, begidx, endidx) | ||
| yield from self.completion_matches | ||
| class SwitchInput(ast.NodeTransformer): | ||
| def visit_Call(self,node): | ||
| if isinstance(node.func,ast.Name) and node.func.id == 'input': | ||
| load = ast.Load() | ||
| return ast.Call( | ||
| func = ast.Attribute( | ||
| value=ast.Name( | ||
| id='self', | ||
| ctx=load, | ||
| ), | ||
| attr='input_method', | ||
| ctx=load, | ||
| ), | ||
| args=node.args, | ||
| keywords=node.keywords, | ||
| ) | ||
| else: | ||
| return node | ||
| ptcmd_tree = ast.parse(inspect.getsource(PTCmd)) | ||
| cmd_tree = ast.fix_missing_locations(SwitchInput().visit(ast.parse(inspect.getsource(cmd.Cmd)))) #get a version that swaps all input(...) calls with self.input_method(...) | ||
| #find the cmdloop function | ||
| found = False | ||
| for node in ast.walk(cmd_tree): | ||
| if isinstance(node,ast.FunctionDef) and node.name == 'cmdloop': | ||
| cmdloop_node = node | ||
| found = True | ||
| break | ||
| assert(found) | ||
| #find the PTCmd class | ||
| found = False | ||
| for node in ast.walk(ptcmd_tree): | ||
| if isinstance(node,ast.ClassDef) and node.name == 'PTCmd': | ||
| found = True | ||
| ptcmd_node = node | ||
| break | ||
| assert(found) | ||
| #add the cmdloop function to the class definition (overwrite inherited version) | ||
| ptcmd_node.body.append(cmdloop_node) | ||
| ptcmd_tree = ast.fix_missing_locations(ptcmd_tree) | ||
| #Redefine the new class | ||
| exec(compile(ptcmd_tree,'<ast>','exec'))</code></pre> | ||
| </details> | ||
| </section> | ||
| <section> | ||
| </section> | ||
| <section> | ||
| </section> | ||
| <section> | ||
| </section> | ||
| <section> | ||
| <h2 class="section-title" id="header-classes">Classes</h2> | ||
| <dl> | ||
| <dt id="devshell.ptcmd.PTCmd"><code class="flex name class"> | ||
| <span>class <span class="ident">PTCmd</span></span> | ||
| <span>(</span><span>completekey='tab', stdin=None, stdout=None, **psession_kwargs)</span> | ||
| </code></dt> | ||
| <dd> | ||
| <div class="desc"><p>A simple framework for writing line-oriented command interpreters.</p> | ||
| <p>These are often useful for test harnesses, administrative tools, and | ||
| prototypes that will later be wrapped in a more sophisticated interface.</p> | ||
| <p>A Cmd instance or subclass instance is a line-oriented interpreter | ||
| framework. | ||
| There is no good reason to instantiate Cmd itself; rather, | ||
| it's useful as a superclass of an interpreter class you define yourself | ||
| in order to inherit Cmd's methods and encapsulate action methods.</p> | ||
| <p>Instantiate a line-oriented interpreter framework.</p> | ||
| <p>The optional argument 'completekey' is the readline name of a | ||
| completion key; it defaults to the Tab key. If completekey is | ||
| not None and the readline module is available, command completion | ||
| is done automatically. The optional arguments stdin and stdout | ||
| specify alternate input and output file objects; if not specified, | ||
| sys.stdin and sys.stdout are used.</p></div> | ||
| <details class="source"> | ||
| <summary> | ||
| <span>Expand source code</span> | ||
| </summary> | ||
| <pre><code class="python">class PTCmd(cmd.Cmd): | ||
| def __init__(self, completekey='tab', stdin=None, stdout=None,**psession_kwargs): | ||
| super().__init__(completekey,stdin,stdout) | ||
| psession_kwargs['completer'] = PTCmd_Completer(self) | ||
| psession_kwargs['complete_while_typing'] = True | ||
| self.psession = PromptSession(**psession_kwargs) | ||
| self.input_method = self.psession.prompt | ||
| def pt_complete(self, document,complete_event): | ||
| origline = document.text | ||
| line = origline.lstrip() | ||
| stripped = len(origline) - len(line) | ||
| begidx = document.cursor_position_col - stripped | ||
| endidx = len(document.text) - stripped | ||
| if begidx>0: | ||
| cmd, args, foo = self.parseline(line) | ||
| if cmd == '': | ||
| compfunc = self.completedefault | ||
| else: | ||
| try: | ||
| compfunc = getattr(self, 'complete_' + cmd) | ||
| except AttributeError: | ||
| compfunc = self.completedefault | ||
| else: | ||
| compfunc = self.completenames | ||
| self.completion_matches = compfunc(document.text, line, begidx, endidx) | ||
| yield from self.completion_matches</code></pre> | ||
| </details> | ||
| <h3>Ancestors</h3> | ||
| <ul class="hlist"> | ||
| <li>cmd.Cmd</li> | ||
| </ul> | ||
| <h3>Subclasses</h3> | ||
| <ul class="hlist"> | ||
| <li><a title="devshell.shell.DevshellCmd" href="shell.html#devshell.shell.DevshellCmd">DevshellCmd</a></li> | ||
| </ul> | ||
| <h3>Methods</h3> | ||
| <dl> | ||
| <dt id="devshell.ptcmd.PTCmd.cmdloop"><code class="name flex"> | ||
| <span>def <span class="ident">cmdloop</span></span>(<span>self, intro=None)</span> | ||
| </code></dt> | ||
| <dd> | ||
| <div class="desc"><p>Repeatedly issue a prompt, accept input, parse an initial prefix | ||
| off the received input, and dispatch to action methods, passing them | ||
| the remainder of the line as argument.</p></div> | ||
| </dd> | ||
| <dt id="devshell.ptcmd.PTCmd.pt_complete"><code class="name flex"> | ||
| <span>def <span class="ident">pt_complete</span></span>(<span>self, document, complete_event)</span> | ||
| </code></dt> | ||
| <dd> | ||
| <div class="desc"></div> | ||
| </dd> | ||
| </dl> | ||
| </dd> | ||
| <dt id="devshell.ptcmd.PTCmd_Completer"><code class="flex name class"> | ||
| <span>class <span class="ident">PTCmd_Completer</span></span> | ||
| <span>(</span><span>ptcmd)</span> | ||
| </code></dt> | ||
| <dd> | ||
| <div class="desc"><p>Base class for completer implementations.</p></div> | ||
| <details class="source"> | ||
| <summary> | ||
| <span>Expand source code</span> | ||
| </summary> | ||
| <pre><code class="python">class PTCmd_Completer(Completer): | ||
| def __init__(self,ptcmd): | ||
| self.ptcmd = ptcmd | ||
| def get_completions(self,document,complete_event): | ||
| for suggestion in self.ptcmd.pt_complete(document,complete_event): | ||
| #yield Completion(suggestion,start_position=0) | ||
| try: | ||
| yield Completion(suggestion,-len(shlex.split(document.current_line_before_cursor)[-1])) | ||
| except: | ||
| pass</code></pre> | ||
| </details> | ||
| <h3>Ancestors</h3> | ||
| <ul class="hlist"> | ||
| <li>prompt_toolkit.completion.base.Completer</li> | ||
| </ul> | ||
| <h3>Methods</h3> | ||
| <dl> | ||
| <dt id="devshell.ptcmd.PTCmd_Completer.get_completions"><code class="name flex"> | ||
| <span>def <span class="ident">get_completions</span></span>(<span>self, document, complete_event)</span> | ||
| </code></dt> | ||
| <dd> | ||
| <div class="desc"><p>This should be a generator that yields :class:<code>.Completion</code> instances.</p> | ||
| <p>If the generation of completions is something expensive (that takes a | ||
| lot of time), consider wrapping this <code>Completer</code> class in a | ||
| <code>ThreadedCompleter</code>. In that case, the completer algorithm runs in a | ||
| background thread and completions will be displayed as soon as they | ||
| arrive.</p> | ||
| <p>:param document: :class:<code>~prompt_toolkit.document.Document</code> instance. | ||
| :param complete_event: :class:<code>.CompleteEvent</code> instance.</p></div> | ||
| <details class="source"> | ||
| <summary> | ||
| <span>Expand source code</span> | ||
| </summary> | ||
| <pre><code class="python">def get_completions(self,document,complete_event): | ||
| for suggestion in self.ptcmd.pt_complete(document,complete_event): | ||
| #yield Completion(suggestion,start_position=0) | ||
| try: | ||
| yield Completion(suggestion,-len(shlex.split(document.current_line_before_cursor)[-1])) | ||
| except: | ||
| pass</code></pre> | ||
| </details> | ||
| </dd> | ||
| </dl> | ||
| </dd> | ||
| <dt id="devshell.ptcmd.SwitchInput"><code class="flex name class"> | ||
| <span>class <span class="ident">SwitchInput</span></span> | ||
| </code></dt> | ||
| <dd> | ||
| <div class="desc"><p>A :class:<code>NodeVisitor</code> subclass that walks the abstract syntax tree and | ||
| allows modification of nodes.</p> | ||
| <p>The <code>NodeTransformer</code> will walk the AST and use the return value of the | ||
| visitor methods to replace or remove the old node. | ||
| If the return value of | ||
| the visitor method is <code>None</code>, the node will be removed from its location, | ||
| otherwise it is replaced with the return value. | ||
| The return value may be the | ||
| original node in which case no replacement takes place.</p> | ||
| <p>Here is an example transformer that rewrites all occurrences of name lookups | ||
| (<code>foo</code>) to <code>data['foo']</code>::</p> | ||
| <p>class RewriteName(NodeTransformer):</p> | ||
| <pre><code> def visit_Name(self, node): | ||
| return Subscript( | ||
| value=Name(id='data', ctx=Load()), | ||
| slice=Index(value=Str(s=node.id)), | ||
| ctx=node.ctx | ||
| ) | ||
| </code></pre> | ||
| <p>Keep in mind that if the node you're operating on has child nodes you must | ||
| either transform the child nodes yourself or call the :meth:<code>generic_visit</code> | ||
| method for the node first.</p> | ||
| <p>For nodes that were part of a collection of statements (that applies to all | ||
| statement nodes), the visitor may also return a list of nodes rather than | ||
| just a single node.</p> | ||
| <p>Usually you use the transformer like this::</p> | ||
| <p>node = YourTransformer().visit(node)</p></div> | ||
| <details class="source"> | ||
| <summary> | ||
| <span>Expand source code</span> | ||
| </summary> | ||
| <pre><code class="python">class SwitchInput(ast.NodeTransformer): | ||
| def visit_Call(self,node): | ||
| if isinstance(node.func,ast.Name) and node.func.id == 'input': | ||
| load = ast.Load() | ||
| return ast.Call( | ||
| func = ast.Attribute( | ||
| value=ast.Name( | ||
| id='self', | ||
| ctx=load, | ||
| ), | ||
| attr='input_method', | ||
| ctx=load, | ||
| ), | ||
| args=node.args, | ||
| keywords=node.keywords, | ||
| ) | ||
| else: | ||
| return node</code></pre> | ||
| </details> | ||
| <h3>Ancestors</h3> | ||
| <ul class="hlist"> | ||
| <li>ast.NodeTransformer</li> | ||
| <li>ast.NodeVisitor</li> | ||
| </ul> | ||
| <h3>Methods</h3> | ||
| <dl> | ||
| <dt id="devshell.ptcmd.SwitchInput.visit_Call"><code class="name flex"> | ||
| <span>def <span class="ident">visit_Call</span></span>(<span>self, node)</span> | ||
| </code></dt> | ||
| <dd> | ||
| <div class="desc"></div> | ||
| <details class="source"> | ||
| <summary> | ||
| <span>Expand source code</span> | ||
| </summary> | ||
| <pre><code class="python">def visit_Call(self,node): | ||
| if isinstance(node.func,ast.Name) and node.func.id == 'input': | ||
| load = ast.Load() | ||
| return ast.Call( | ||
| func = ast.Attribute( | ||
| value=ast.Name( | ||
| id='self', | ||
| ctx=load, | ||
| ), | ||
| attr='input_method', | ||
| ctx=load, | ||
| ), | ||
| args=node.args, | ||
| keywords=node.keywords, | ||
| ) | ||
| else: | ||
| return node</code></pre> | ||
| </details> | ||
| </dd> | ||
| </dl> | ||
| </dd> | ||
| </dl> | ||
| </section> | ||
| </article> | ||
| <nav id="sidebar"> | ||
| <h1>Index</h1> | ||
| <div class="toc"> | ||
| <ul></ul> | ||
| </div> | ||
| <ul id="index"> | ||
| <li><h3>Super-module</h3> | ||
| <ul> | ||
| <li><code><a title="devshell" href="index.html">devshell</a></code></li> | ||
| </ul> | ||
| </li> | ||
| <li><h3><a href="#header-classes">Classes</a></h3> | ||
| <ul> | ||
| <li> | ||
| <h4><code><a title="devshell.ptcmd.PTCmd" href="#devshell.ptcmd.PTCmd">PTCmd</a></code></h4> | ||
| <ul class=""> | ||
| <li><code><a title="devshell.ptcmd.PTCmd.cmdloop" href="#devshell.ptcmd.PTCmd.cmdloop">cmdloop</a></code></li> | ||
| <li><code><a title="devshell.ptcmd.PTCmd.pt_complete" href="#devshell.ptcmd.PTCmd.pt_complete">pt_complete</a></code></li> | ||
| </ul> | ||
| </li> | ||
| <li> | ||
| <h4><code><a title="devshell.ptcmd.PTCmd_Completer" href="#devshell.ptcmd.PTCmd_Completer">PTCmd_Completer</a></code></h4> | ||
| <ul class=""> | ||
| <li><code><a title="devshell.ptcmd.PTCmd_Completer.get_completions" href="#devshell.ptcmd.PTCmd_Completer.get_completions">get_completions</a></code></li> | ||
| </ul> | ||
| </li> | ||
| <li> | ||
| <h4><code><a title="devshell.ptcmd.SwitchInput" href="#devshell.ptcmd.SwitchInput">SwitchInput</a></code></h4> | ||
| <ul class=""> | ||
| <li><code><a title="devshell.ptcmd.SwitchInput.visit_Call" href="#devshell.ptcmd.SwitchInput.visit_Call">visit_Call</a></code></li> | ||
| </ul> | ||
| </li> | ||
| </ul> | ||
| </li> | ||
| </ul> | ||
| </nav> | ||
| </main> | ||
| <footer id="footer"> | ||
| <p>Generated by <a href="https://pdoc3.github.io/pdoc"><cite>pdoc</cite> 0.9.2</a>.</p> | ||
| </footer> | ||
| </body> | ||
| </html> |
Sorry, the diff of this file is too big to display
+21
| Copyright (c) 2019 U.N. Owen | ||
| MIT License | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
+15
| files = setup.py src/devshell/*.py | ||
| all: do_build do_upload do_install | ||
| do_build: $(files) | ||
| echo "building" | ||
| if [ -d ./build ]; then rm -rf ./build; fi | ||
| if [ -d ./dist ]; then rm -rf ./dist; fi | ||
| if [ -d ./devshell.egg-info ]; then rm -rf ./devshell.egg-info; fi | ||
| pdoc --html --force --output-dir docs src/devshell | ||
| python3 setup.py sdist bdist_wheel | ||
| do_upload: do_build | ||
| echo "uploading" | ||
| python3 -m twine upload dist/* | ||
| do_install: do_upload | ||
| echo "installing" | ||
| python3 -m pip install --user --upgrade --force-reinstall devshell |
Sorry, the diff of this file is not supported yet
| """ | ||
| This script takes the Cmd object from the built-in standard library, adds an input_method attribute, and replaces all calls to the standard input() with calls to the input_method attribute | ||
| """ | ||
| import ast, inspect, cmd, shlex | ||
| from prompt_toolkit.completion import Completer, Completion | ||
| from prompt_toolkit import PromptSession | ||
| class PTCmd_Completer(Completer): | ||
| def __init__(self,ptcmd): | ||
| self.ptcmd = ptcmd | ||
| def get_completions(self,document,complete_event): | ||
| for suggestion in self.ptcmd.pt_complete(document,complete_event): | ||
| #yield Completion(suggestion,start_position=0) | ||
| try: | ||
| yield Completion(suggestion,-len(shlex.split(document.current_line_before_cursor)[-1])) | ||
| except: | ||
| pass | ||
| class PTCmd(cmd.Cmd): | ||
| def __init__(self, completekey='tab', stdin=None, stdout=None,**psession_kwargs): | ||
| super().__init__(completekey,stdin,stdout) | ||
| psession_kwargs['completer'] = PTCmd_Completer(self) | ||
| psession_kwargs['complete_while_typing'] = True | ||
| self.psession = PromptSession(**psession_kwargs) | ||
| self.input_method = self.psession.prompt | ||
| def pt_complete(self, document,complete_event): | ||
| origline = document.text | ||
| line = origline.lstrip() | ||
| stripped = len(origline) - len(line) | ||
| begidx = document.cursor_position_col - stripped | ||
| endidx = len(document.text) - stripped | ||
| if begidx>0: | ||
| cmd, args, foo = self.parseline(line) | ||
| if cmd == '': | ||
| compfunc = self.completedefault | ||
| else: | ||
| try: | ||
| compfunc = getattr(self, 'complete_' + cmd) | ||
| except AttributeError: | ||
| compfunc = self.completedefault | ||
| else: | ||
| compfunc = self.completenames | ||
| self.completion_matches = compfunc(document.text, line, begidx, endidx) | ||
| yield from self.completion_matches | ||
| class SwitchInput(ast.NodeTransformer): | ||
| def visit_Call(self,node): | ||
| if isinstance(node.func,ast.Name) and node.func.id == 'input': | ||
| load = ast.Load() | ||
| return ast.Call( | ||
| func = ast.Attribute( | ||
| value=ast.Name( | ||
| id='self', | ||
| ctx=load, | ||
| ), | ||
| attr='input_method', | ||
| ctx=load, | ||
| ), | ||
| args=node.args, | ||
| keywords=node.keywords, | ||
| ) | ||
| else: | ||
| return node | ||
| ptcmd_tree = ast.parse(inspect.getsource(PTCmd)) | ||
| cmd_tree = ast.fix_missing_locations(SwitchInput().visit(ast.parse(inspect.getsource(cmd.Cmd)))) #get a version that swaps all input(...) calls with self.input_method(...) | ||
| #find the cmdloop function | ||
| found = False | ||
| for node in ast.walk(cmd_tree): | ||
| if isinstance(node,ast.FunctionDef) and node.name == 'cmdloop': | ||
| cmdloop_node = node | ||
| found = True | ||
| break | ||
| assert(found) | ||
| #find the PTCmd class | ||
| found = False | ||
| for node in ast.walk(ptcmd_tree): | ||
| if isinstance(node,ast.ClassDef) and node.name == 'PTCmd': | ||
| found = True | ||
| ptcmd_node = node | ||
| break | ||
| assert(found) | ||
| #add the cmdloop function to the class definition (overwrite inherited version) | ||
| ptcmd_node.body.append(cmdloop_node) | ||
| ptcmd_tree = ast.fix_missing_locations(ptcmd_tree) | ||
| #Redefine the new class | ||
| exec(compile(ptcmd_tree,'<ast>','exec')) | ||
+61
-5
| Metadata-Version: 2.1 | ||
| Name: devshell | ||
| Version: 0.0.2 | ||
| Version: 0.0.3 | ||
| Summary: Shell-like tool to make it easier to develop python code | ||
@@ -13,8 +13,64 @@ Home-page: https://github.com/mmiguel6288code/devshell | ||
| devshell is a tool to make python development easier. | ||
| [View API documentation](http://htmlpreview.github.io/?https://github.com/mmiguel6288code/devshell/blob/master/docs/devshell/index.html) | ||
| ## Key Features | ||
| 1. Enables entering an interactive session tied to the docstring of a particular python object. Inputs and responses are automatically recorded and inserted into the docstring as doctests. | ||
| 2. Shell-like tool for navigating, inspecting, testing, and making doctests for python objects defined in a project | ||
| devshell provides the power user terminal python developer feeling. | ||
| There's normal basic shell navigation with cd, ls, pwd, and then there are python versions of those for navigating through a code tree: pcd, pls, ppwd. | ||
| What is a code tree? It is the following types of code blocks: | ||
| 1. Package | ||
| 2. Module | ||
| 3. Class | ||
| 4. Function/Method/Coroutine | ||
| ```bash | ||
| $ cd ~/projects/statopy | ||
| $ ls | ||
| LICENSE __pycache__ statopy.py | ||
| $ python3 -m devshell | ||
| Starting devshell command line interface... | ||
| devshell version 0.0.3 | ||
| Welcome to devshell. Type help or ? to list commands. Start a line with ! to execute a shell command in a sub-shell (does not retain environmental variables). | ||
| (devshell)$ | ||
| ``` | ||
| If a package or module in your current working directory (the normal type affected by cd and reported with pwd), then those will show up when you type pls. | ||
| You can enter your "python location" into it via pcd and check your current python location with ppwd. | ||
| ```bash | ||
| (devshell)$ pls | ||
| statopy module directory | ||
| (devshell)$ pcd statopy | ||
| (devshell)$ pls | ||
| ScalarProbModel class directory | ||
| ScalarRegression class directory | ||
| ScalarStats class directory | ||
| VectorStats class directory | ||
| (devshell)$ pcd ScalarStats | ||
| (devshell)$ pls | ||
| __add__ function non-directory | ||
| __init__ function non-directory | ||
| __setattr__ function non-directory | ||
| consume function non-directory | ||
| update function non-directory | ||
| (devshell)$ ppwd | ||
| /statopy.ScalarStats (class) | ||
| (devshell)$ | ||
| ``` | ||
| That's nice, but what can you do besides inspecting what code blocks exist? | ||
| ```bash | ||
| (devshell)$ help | ||
| Documented commands (type help <topic>): | ||
| ======================================== | ||
| EOF create doctestify h mv pwd read source | ||
| activate deactivate edit help pcd pytest restart venv | ||
| cd debug editvim interactive pip python rm | ||
| coverage doc exit ls pls q rmtree | ||
| cp doctest grep mkdir ppwd quit run | ||
| ``` | ||
| ## What are doctests and why should I care? | ||
@@ -21,0 +77,0 @@ Doctests are snippets of text that resemble a Python interactive mode session. |
+60
-4
| # devshell | ||
| devshell is a tool to make python development easier. | ||
| [View API documentation](http://htmlpreview.github.io/?https://github.com/mmiguel6288code/devshell/blob/master/docs/devshell/index.html) | ||
| ## Key Features | ||
| 1. Enables entering an interactive session tied to the docstring of a particular python object. Inputs and responses are automatically recorded and inserted into the docstring as doctests. | ||
| 2. Shell-like tool for navigating, inspecting, testing, and making doctests for python objects defined in a project | ||
| devshell provides the power user terminal python developer feeling. | ||
| There's normal basic shell navigation with cd, ls, pwd, and then there are python versions of those for navigating through a code tree: pcd, pls, ppwd. | ||
| What is a code tree? It is the following types of code blocks: | ||
| 1. Package | ||
| 2. Module | ||
| 3. Class | ||
| 4. Function/Method/Coroutine | ||
| ```bash | ||
| $ cd ~/projects/statopy | ||
| $ ls | ||
| LICENSE __pycache__ statopy.py | ||
| $ python3 -m devshell | ||
| Starting devshell command line interface... | ||
| devshell version 0.0.3 | ||
| Welcome to devshell. Type help or ? to list commands. Start a line with ! to execute a shell command in a sub-shell (does not retain environmental variables). | ||
| (devshell)$ | ||
| ``` | ||
| If a package or module in your current working directory (the normal type affected by cd and reported with pwd), then those will show up when you type pls. | ||
| You can enter your "python location" into it via pcd and check your current python location with ppwd. | ||
| ```bash | ||
| (devshell)$ pls | ||
| statopy module directory | ||
| (devshell)$ pcd statopy | ||
| (devshell)$ pls | ||
| ScalarProbModel class directory | ||
| ScalarRegression class directory | ||
| ScalarStats class directory | ||
| VectorStats class directory | ||
| (devshell)$ pcd ScalarStats | ||
| (devshell)$ pls | ||
| __add__ function non-directory | ||
| __init__ function non-directory | ||
| __setattr__ function non-directory | ||
| consume function non-directory | ||
| update function non-directory | ||
| (devshell)$ ppwd | ||
| /statopy.ScalarStats (class) | ||
| (devshell)$ | ||
| ``` | ||
| That's nice, but what can you do besides inspecting what code blocks exist? | ||
| ```bash | ||
| (devshell)$ help | ||
| Documented commands (type help <topic>): | ||
| ======================================== | ||
| EOF create doctestify h mv pwd read source | ||
| activate deactivate edit help pcd pytest restart venv | ||
| cd debug editvim interactive pip python rm | ||
| coverage doc exit ls pls q rmtree | ||
| cp doctest grep mkdir ppwd quit run | ||
| ``` | ||
| ## What are doctests and why should I care? | ||
@@ -10,0 +66,0 @@ Doctests are snippets of text that resemble a Python interactive mode session. |
+1
-1
@@ -14,3 +14,3 @@ from io import open | ||
| REQUIRES = ['pypager','pytest','coverage'] | ||
| REQUIRES = ['pypager','pytest','coverage','prompt-toolkit','pdoc3'] | ||
@@ -17,0 +17,0 @@ setup( |
| Metadata-Version: 2.1 | ||
| Name: devshell | ||
| Version: 0.0.2 | ||
| Version: 0.0.3 | ||
| Summary: Shell-like tool to make it easier to develop python code | ||
@@ -13,8 +13,64 @@ Home-page: https://github.com/mmiguel6288code/devshell | ||
| devshell is a tool to make python development easier. | ||
| [View API documentation](http://htmlpreview.github.io/?https://github.com/mmiguel6288code/devshell/blob/master/docs/devshell/index.html) | ||
| ## Key Features | ||
| 1. Enables entering an interactive session tied to the docstring of a particular python object. Inputs and responses are automatically recorded and inserted into the docstring as doctests. | ||
| 2. Shell-like tool for navigating, inspecting, testing, and making doctests for python objects defined in a project | ||
| devshell provides the power user terminal python developer feeling. | ||
| There's normal basic shell navigation with cd, ls, pwd, and then there are python versions of those for navigating through a code tree: pcd, pls, ppwd. | ||
| What is a code tree? It is the following types of code blocks: | ||
| 1. Package | ||
| 2. Module | ||
| 3. Class | ||
| 4. Function/Method/Coroutine | ||
| ```bash | ||
| $ cd ~/projects/statopy | ||
| $ ls | ||
| LICENSE __pycache__ statopy.py | ||
| $ python3 -m devshell | ||
| Starting devshell command line interface... | ||
| devshell version 0.0.3 | ||
| Welcome to devshell. Type help or ? to list commands. Start a line with ! to execute a shell command in a sub-shell (does not retain environmental variables). | ||
| (devshell)$ | ||
| ``` | ||
| If a package or module in your current working directory (the normal type affected by cd and reported with pwd), then those will show up when you type pls. | ||
| You can enter your "python location" into it via pcd and check your current python location with ppwd. | ||
| ```bash | ||
| (devshell)$ pls | ||
| statopy module directory | ||
| (devshell)$ pcd statopy | ||
| (devshell)$ pls | ||
| ScalarProbModel class directory | ||
| ScalarRegression class directory | ||
| ScalarStats class directory | ||
| VectorStats class directory | ||
| (devshell)$ pcd ScalarStats | ||
| (devshell)$ pls | ||
| __add__ function non-directory | ||
| __init__ function non-directory | ||
| __setattr__ function non-directory | ||
| consume function non-directory | ||
| update function non-directory | ||
| (devshell)$ ppwd | ||
| /statopy.ScalarStats (class) | ||
| (devshell)$ | ||
| ``` | ||
| That's nice, but what can you do besides inspecting what code blocks exist? | ||
| ```bash | ||
| (devshell)$ help | ||
| Documented commands (type help <topic>): | ||
| ======================================== | ||
| EOF create doctestify h mv pwd read source | ||
| activate deactivate edit help pcd pytest restart venv | ||
| cd debug editvim interactive pip python rm | ||
| coverage doc exit ls pls q rmtree | ||
| cp doctest grep mkdir ppwd quit run | ||
| ``` | ||
| ## What are doctests and why should I care? | ||
@@ -21,0 +77,0 @@ Doctests are snippets of text that resemble a Python interactive mode session. |
| pypager | ||
| pytest | ||
| coverage | ||
| prompt-toolkit | ||
| pdoc3 |
@@ -0,6 +1,14 @@ | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
| notes | ||
| setup.py | ||
| docs/devshell/index.html | ||
| docs/devshell/injector.html | ||
| docs/devshell/ptcmd.html | ||
| docs/devshell/shell.html | ||
| src/devshell/__init__.py | ||
| src/devshell/__main__.py | ||
| src/devshell/injector.py | ||
| src/devshell/ptcmd.py | ||
| src/devshell/shell.py | ||
@@ -11,2 +19,3 @@ src/devshell.egg-info/PKG-INFO | ||
| src/devshell.egg-info/requires.txt | ||
| src/devshell.egg-info/top_level.txt | ||
| src/devshell.egg-info/top_level.txt | ||
| tests/__init__.py |
@@ -1,3 +0,4 @@ | ||
| __version__ = '0.0.2' | ||
| __version__ = '0.0.3' | ||
| from .injector import doctestify, set_end_interactive | ||
| from .shell import DevshellCmd | ||
+34
-24
@@ -32,2 +32,3 @@ import inspect, ast, re, sys, code, readline, importlib, os, doctest, os.path | ||
| get_target.__annotations__ = {'target_fqn':'fully qualified name of target','return':('target object','top-level target module','fully qualified name of module')} | ||
| class _ModStdout(object): | ||
@@ -83,3 +84,3 @@ def __init__(self,iobuf): | ||
| return ast_obj, filepath,source | ||
| return ast_obj, filepath,source, src_lines | ||
@@ -96,3 +97,3 @@ class DoctestInjector(object): | ||
| self.module_fqn = module_fqn | ||
| ast_obj,self.filepath,self.original_source = get_ast_obj(target_fqn,obj,module,module_fqn) | ||
| ast_obj,self.filepath,self.original_source,self.src_lines = get_ast_obj(target_fqn,obj,module,module_fqn) | ||
@@ -110,6 +111,6 @@ if isinstance(ast_obj.body[0],ast.Expr) and isinstance(ast_obj.body[0].value,ast.Str): | ||
| byte_index = ast_doc.col_offset | ||
| indentation = re.search('^\\s*',src_lines[line_index]).group(0) #use docstring end line to determine indentation | ||
| newline = re.search('[\r\n]+$',src_lines[line_index]).group(0) #use docstring end line to determine newline | ||
| top = src_lines[:line_index+1] #include entire docstring including end line | ||
| bottom = src_lines[line_index+1:] #everything else | ||
| indentation = re.search('^\\s*',self.src_lines[line_index]).group(0) #use docstring end line to determine indentation | ||
| newline = re.search('[\r\n]+$',self.src_lines[line_index]).group(0) #use docstring end line to determine newline | ||
| top = self.src_lines[:line_index+1] #include entire docstring including end line | ||
| bottom = self.src_lines[line_index+1:] #everything else | ||
| ending = '"""'+top[-1].split('"""')[-1] #get the trailing quotes and characters after doctsring | ||
@@ -122,5 +123,5 @@ top[-1] = top[-1][len(indentation):-len(ending)]+newline #remove trailing from top | ||
| line_index = ast_obj.lineno-1 #line of function | ||
| indentation = re.search('^\\s*',src_lines[line_index]).group(0).strip('\r\n')+' ' #use indentation of function plus four spaces | ||
| newline = re.search('[\r\n]+$',src_lines[line_index]).group(0) #use newline of function line | ||
| top = src_lines[:line_index+1] #include funtion | ||
| indentation = re.search('^\\s*',self.src_lines[line_index]).group(0).strip('\r\n')+' ' #use indentation of function plus four spaces | ||
| newline = re.search('[\r\n]+$',self.src_lines[line_index]).group(0) #use newline of function line | ||
| top = self.src_lines[:line_index+1] #include funtion | ||
| bottom = src_lines[ast_obj.lineno:] #everything after function | ||
@@ -139,7 +140,7 @@ ast_first = ast_obj.body[0] #first (and only) element in body | ||
| byte_index = ast_first.col_offset | ||
| indentation = re.search('^\\s*',src_lines[line_index]).group(0) #use first element line to determine indentation | ||
| newline = re.search('[\r\n]+$',src_lines[line_index]).group(0) #use first element line to determine newline | ||
| top = src_lines[:line_index] #everything up to but not including first element | ||
| indentation = re.search('^\\s*',self.src_lines[line_index]).group(0) #use first element line to determine indentation | ||
| newline = re.search('[\r\n]+$',self.src_lines[line_index]).group(0) #use first element line to determine newline | ||
| top = self.src_lines[:line_index] #everything up to but not including first element | ||
| top.append(indentation+'"""'+newline) #add new docstring starting quotes | ||
| bottom = src_lines[ast_obj.body[0].lineno-1:] #first element and everything after | ||
| bottom = self.src_lines[ast_obj.body[0].lineno-1:] #first element and everything after | ||
| bottom.insert(0,indentation+'"""'+newline) #add docstring ending quotes | ||
@@ -167,3 +168,3 @@ self.top = top | ||
| return ''.join(self.top+ indented_middle + self.bottom) | ||
| def doctest_console(self): | ||
| def doctest_console(self,resume=False): | ||
| """ | ||
@@ -175,12 +176,21 @@ This function runs doctests on the target file, loads the file, and enters a special interactive mode with inputs/outputs being recorded. | ||
| """ | ||
| print('Testing doctest execution of original file') | ||
| print('='*80+'\nDoctestify:\n Testing doctest execution of original file') | ||
| oldfailcount,oldtestcount = self.testmod() | ||
| print('...done: Fail count = %d, Total count = %d' % (oldfailcount,oldtestcount)) | ||
| print('Entering interactive console') | ||
| banner='''Doctest insertion targeting object %s within %s | ||
| Press Ctrl+D to stop writing code and incorporate session into the docstring of the targeted object | ||
| To abort this session without writing anything into the targeted file, call the exit() function | ||
| >>> from %s import * # automatic import by devshell''' % (self.target_fqn,self.filepath,self.module_fqn) | ||
| print(' ...done: Fail count = %d, Total count = %d' % (oldfailcount,oldtestcount)) | ||
| print('='*80+'''\nEntering interactive console: | ||
| Target: %s | ||
| File: %s | ||
| (*) Press Ctrl+D to exit and incorporate session into the targeted docstring | ||
| (*) To abort without incorporating anything, call the exit() function | ||
| ''' % (self.target_fqn,self.filepath) + '-'*80) | ||
| console = code.InteractiveConsole() | ||
| print('>>> from %s import * # automatic import by devshell''' % (self.module_fqn)) | ||
| console.push('from %s import *' % (self.module_fqn)) | ||
| if resume: | ||
| doc = inspect.getdoc(self.obj) | ||
| for line in doc.splitlines(): | ||
| line = line.lstrip() | ||
| if line.startswith('>>> ') or line.startswith('... '): | ||
| print(line) | ||
| console.push(line[4:]) | ||
| iobuf = self.middle | ||
@@ -200,3 +210,3 @@ _modstdout = _ModStdout(iobuf) | ||
| #sys.stderr = _modstderr | ||
| console.interact(banner=banner) | ||
| console.interact(banner='') | ||
| sys.stdout = sys.__stdout__ | ||
@@ -246,3 +256,3 @@ sys.stderr = sys.__stderr__ | ||
| del os.environ['PYTHONINSPECT'] | ||
| def doctestify(target_fqn): | ||
| def doctestify(target_fqn,resume=False): | ||
| """ | ||
@@ -253,3 +263,3 @@ Start an interactive recording session for the item identified by the given fully qualified name. | ||
| di = DoctestInjector(target_fqn) | ||
| di.doctest_console() | ||
| di.doctest_console(resume) | ||
+265
-166
@@ -0,4 +1,7 @@ | ||
| #if you go inside a directory, the python part of things shoud stay up at the top package level | ||
| import os, pkgutil, os.path, readline, inspect, doctest, sys, re, importlib, pdb, traceback, code, subprocess, shutil,textwrap, argparse, shlex | ||
| from contextlib import contextmanager | ||
| from cmd import Cmd | ||
| #from cmd import Cmd | ||
| from pypager.pager import Pager | ||
@@ -8,5 +11,14 @@ from pypager.source import StringSource | ||
| from .injector import doctestify, get_target, get_ast_obj | ||
| from .ptcmd import PTCmd | ||
| from . import __version__ | ||
| from prompt_toolkit.shortcuts import PromptSession | ||
| from prompt_toolkit.styles import Style | ||
| from pygments.lexers.shell import BashLexer, BatchLexer, FishShellLexer, PowerShellLexer, TcshLexer | ||
| from pygments.lexers.python import PythonConsoleLexer, PythonLexer | ||
| from prompt_toolkit.lexers import PygmentsLexer | ||
| @contextmanager | ||
@@ -56,41 +68,57 @@ def capture_stdout(): | ||
| class DevshellCmd(Cmd,object): | ||
| class DevshellCmd(PTCmd): | ||
| """ | ||
| This implements the command line interface for devshell | ||
| """ | ||
| prompt = '(devshell)$ ' | ||
| prompt = [('class:prompt','(devshell)$ ')] | ||
| intro = 'devshell version %s\nWelcome to devshell. Type help or ? to list commands. Start a line with ! to execute a shell command in a sub-shell (does not retain environmental variables).\n' % __version__ | ||
| _cdable = set(['package','module','class','root']) | ||
| _callable = set(['function','method','coroutine','class']) | ||
| def __init__(self,completekey='tab',stdin=None,stdout=None): | ||
| self.cwd = os.getcwd() | ||
| self.orig_sys_path = sys.path | ||
| self.ppwd = [] | ||
| self._pls_cache = None | ||
| self.style = Style.from_dict({ | ||
| 'prompt':'#ff0066', | ||
| }) | ||
| super(DevshellCmd,self).__init__(completekey,stdin,stdout,lexer=PygmentsLexer(BashLexer),style=self.style) | ||
| def _ls(self,args=''): | ||
| def _pls(self,args=''): | ||
| args = args.strip() | ||
| if args != '': | ||
| orig_cwd = self.cwd | ||
| orig_pwd = list(self.pwd) | ||
| self._ls_cache = None | ||
| self.do_cd(args) | ||
| result = self._ls('') | ||
| orig_ppwd = list(self.ppwd) | ||
| self._pls_cache = None | ||
| self.do_pcd(args) | ||
| result = self._pls('') | ||
| self.cwd = orig_cwd | ||
| os.chdir(orig_cwd) | ||
| self.pwd = orig_pwd | ||
| self._ls_cache = None | ||
| self.ppwd = orig_ppwd | ||
| self._pls_cache = None | ||
| return result | ||
| else: | ||
| if self._ls_cache is not None: | ||
| return self._ls_cache | ||
| if len(self.pwd) == 0: | ||
| self._ls_cache = [((mi[1],'package') if mi[2] else (mi[1],'module')) for mi in sorted(pkgutil.iter_modules([self.cwd,os.path.join(self.cwd,'src')]),key=lambda mi: mi[1]) if (mi[1],mi[2]) != ('setup',False)] | ||
| return self._ls_cache | ||
| if self._pls_cache is not None: | ||
| return self._pls_cache | ||
| if len(self.ppwd) == 0: | ||
| self._pls_cache = [((mi[1],'package') if mi[2] else (mi[1],'module')) for mi in sorted(pkgutil.iter_modules([self.cwd,os.path.join(self.cwd,'src')]),key=lambda mi: mi[1]) if (mi[1],mi[2]) != ('setup',False)] | ||
| return self._pls_cache | ||
| else: | ||
| current_name,current_type = self.pwd[-1] | ||
| current_name,current_type = self.ppwd[-1] | ||
| if current_type == 'package': | ||
| _,bottom_folder = os.path.split(self.cwd) | ||
| mod_ppwd = list(self.ppwd) | ||
| for i,(package_name,_) in enumerate(self.ppwd): | ||
| if bottom_folder == package_name: | ||
| mod_ppwd = self.ppwd[i+1:] | ||
| break | ||
| self._ls_cache = [((mi[1],'package') if mi[2] else (mi[1],'module')) for mi in sorted(pkgutil.iter_modules([os.path.join(self.cwd,*[item[0] for item in self.pwd])]),key=lambda mi: mi[1])] | ||
| package_fqn = '.'.join(item[0] for item in self.pwd) | ||
| cwd = os.getcwd() | ||
| if not cwd in sys.path: | ||
| sys.path.insert(0,cwd) | ||
| self._pls_cache = [((mi[1],'package') if mi[2] else (mi[1],'module')) for mi in sorted(pkgutil.iter_modules([os.path.join(self.cwd,*[item[0] for item in mod_ppwd])]),key=lambda mi: mi[1])] | ||
| package_fqn = '.'.join(item[0] for item in self.ppwd) | ||
| try: | ||
@@ -103,3 +131,3 @@ pkg = __import__(package_fqn) | ||
| for item in self.pwd[1:]: | ||
| for item in self.ppwd[1:]: | ||
| pkg = getattr(pkg,item[0]) | ||
@@ -110,12 +138,12 @@ for item_name,item in pkg.__dict__.items(): | ||
| if inspect.isfunction(item): | ||
| self._ls_cache.append((item_name,'function')) | ||
| self._pls_cache.append((item_name,'function')) | ||
| elif hasattr(inspect,'iscoroutinefunction') and inspect.iscoroutinefunction(item): | ||
| self._ls_cache.append((item_name,'coroutine')) | ||
| self._pls_cache.append((item_name,'coroutine')) | ||
| elif inspect.isclass(item): | ||
| self._ls_cache.append((item_name,'class')) | ||
| self._ls_cache.sort() | ||
| return self._ls_cache | ||
| self._pls_cache.append((item_name,'class')) | ||
| self._pls_cache.sort() | ||
| return self._pls_cache | ||
| elif current_type == 'module': | ||
| module_fqn = '.'.join(item[0] for item in self.pwd) | ||
| module_fqn = '.'.join(item[0] for item in self.ppwd) | ||
| try: | ||
@@ -127,5 +155,5 @@ mod = __import__(module_fqn) | ||
| return | ||
| for item in self.pwd[1:]: | ||
| for item in self.ppwd[1:]: | ||
| mod = getattr(mod,item[0]) | ||
| self._ls_cache = [] | ||
| self._pls_cache = [] | ||
| for item_name,item in mod.__dict__.items(): | ||
@@ -135,11 +163,11 @@ if inspect.getmodule(item) != mod: | ||
| if inspect.isfunction(item): | ||
| self._ls_cache.append((item_name,'function')) | ||
| self._pls_cache.append((item_name,'function')) | ||
| elif hasattr(inspect,'iscoroutinefunction') and inspect.iscoroutinefunction(item): | ||
| self._ls_cache.append((item_name,'coroutine')) | ||
| self._pls_cache.append((item_name,'coroutine')) | ||
| elif inspect.isclass(item): | ||
| self._ls_cache.append((item_name,'class')) | ||
| self._ls_cache.sort() | ||
| return self._ls_cache | ||
| self._pls_cache.append((item_name,'class')) | ||
| self._pls_cache.sort() | ||
| return self._pls_cache | ||
| elif current_type == 'class': | ||
| klass_fqn = '.'.join(item[0] for item in self.pwd) | ||
| klass_fqn = '.'.join(item[0] for item in self.ppwd) | ||
| try: | ||
@@ -151,23 +179,18 @@ klass,_,_ = get_target(klass_fqn) | ||
| self._ls_cache = [] | ||
| self._pls_cache = [] | ||
| for item_name,item in klass.__dict__.items(): | ||
| if inspect.ismethod(item): | ||
| self._ls_cache.append((item_name,'method')) | ||
| self._pls_cache.append((item_name,'method')) | ||
| elif inspect.isfunction(item): | ||
| self._ls_cache.append((item_name,'function')) | ||
| self._pls_cache.append((item_name,'function')) | ||
| elif hasattr(inspect,'iscoroutinefunction') and inspect.iscoroutinefunction(item): | ||
| self._ls_cache.append((item_name,'coroutine')) | ||
| self._pls_cache.append((item_name,'coroutine')) | ||
| elif inspect.isclass(item): | ||
| self._ls_cache.append((item_name,'class')) | ||
| self._ls_cache.sort() | ||
| return self._ls_cache | ||
| self._pls_cache.append((item_name,'class')) | ||
| self._pls_cache.sort() | ||
| return self._pls_cache | ||
| else: | ||
| print('Error - cannot perform ls when targeting a %s - try to run "cd .." first' % current_type) | ||
| print('Error - cannot perform pls when targeting a %s - try to run "pcd .." first' % current_type) | ||
| return [] | ||
| def __init__(self,*args,**kwargs): | ||
| self.cwd = os.getcwd() | ||
| self.pwd = [] | ||
| self._ls_cache = None | ||
| super(DevshellCmd,self).__init__(*args,**kwargs) | ||
| def do_h(self,args): | ||
@@ -195,5 +218,52 @@ """ Alias for help """ | ||
| """ | ||
| sys.exit(subprocess.run([sys.executable,'-m','devshell','-d',os.path.abspath(os.getcwd()),'-t','.'.join([item[0] for item in self.pwd])]).returncode) | ||
| sys.exit(subprocess.run([sys.executable,'-m','devshell','-d',os.path.abspath(os.getcwd()),'-t','.'.join([item[0] for item in self.ppwd])]).returncode) | ||
| def do_venv(self,args): | ||
| """ | ||
| Help: (devshell)$ venv [env] | ||
| Creates a virtual environment at the current location with the given name | ||
| If no name is given, the name will be "env" | ||
| """ | ||
| if args.strip() == '': | ||
| args = 'env' | ||
| subprocess.run([sys.executable,'-m','venv',args]) | ||
| def do_activate(): | ||
| """ | ||
| Help: (devshell)$ activate [env] | ||
| Activates the virtual environment at the current location with the given name | ||
| If no name is given, the name will be "env" | ||
| """ | ||
| cwd = os.path.abspath(os.getcwd()) | ||
| ppwd = '.'.join([item[0] for item in self.ppwd]) | ||
| if sys.platform in ['win32','cygwin']: | ||
| subprocess.run(['cmd.exe','/C',r'"{env}\Scripts\activate.bat & {executable} -m devshell -d {cwd} -t {ppwd}"'.format(env=env,executable=os.path.basename(sys.executable),cwd=cwd,ppwd=ppwd)]) | ||
| else: | ||
| default_shell = os.environ['SHELL'] | ||
| shell_name = os.path.basename(default_shell) | ||
| subprocess.run([default_shell,'-c','"source {env}/bin/activate; {executable} -m devshell -d {cwd} -t {ppwd}"'.format(env=env,executable=os.path.basename(sys.executable),cwd=cwd,ppwd=ppwd)]) | ||
| def do_deactivate(): | ||
| """ | ||
| Help: (devshell)$ activate [env] | ||
| Deactivates the current virtual environment | ||
| Note: This will result in changing the current working directory and python target to what they were at the time the virtual environment was activated | ||
| """ | ||
| if 'VIRTUAL_ENV' in os.environ: | ||
| print('Exiting %s' % os.environ['VIRTUAL_ENV']) | ||
| sys.exit(0) | ||
| else: | ||
| print('Not currently in a virtual environment') | ||
| def do_create(self,args): | ||
| """ | ||
| Help: (devshell)$ create project_name | ||
| Creates a new directory with the provided project name | ||
| Creates a src subfolder with an empty python package with the project name | ||
| Creates an empty tests python package | ||
| Creates a setup.py | ||
| Creates a LICENSE file (MIT) | ||
| Creates a Makefile | ||
| Creates a docs subfolder | ||
| Creates a venv env and activates it | ||
| """ | ||
| def do_read(self,args): | ||
@@ -280,11 +350,11 @@ """ | ||
| if len(editor) == 0: | ||
| print('Specify an editor (e.g. vim, nano, notepad++.exe, etc)') | ||
| print('Specify an editor (e.g. edit vim, edit nano, edit notepad++.exe, etc)') | ||
| return | ||
| else: | ||
| editor=[editor] | ||
| target_fqn = '.'.join(item[0] for item in self.pwd) | ||
| target_fqn = '.'.join(item[0] for item in self.ppwd) | ||
| if target_fqn != '': | ||
| current_name,current_type = self.pwd[-1] | ||
| current_name,current_type = self.ppwd[-1] | ||
| if current_type == 'package': | ||
| filepath = os.path.abspath(os.path.join(self.cwd,*target_fqn.split('.')) + '/__init__.py') | ||
| filepath = self._get_path() + '__init__.py' | ||
| if os.path.getsize(filepath) == 0: | ||
@@ -295,3 +365,3 @@ print('File is empty') | ||
| elif current_type == 'module': | ||
| filepath = os.path.abspath(os.path.join(self.cwd,*target_fqn.split('.'))+'.py') | ||
| filepath = self._get_path() + '.py' | ||
| if os.path.getsize(filepath) == 0: | ||
@@ -312,2 +382,17 @@ print('File is empty') | ||
| print('No target identified') | ||
| def _get_path(self): | ||
| ppwd = list(self.ppwd) | ||
| while len(ppwd) > 0 and ppwd[-1][1] not in ['package','module']: | ||
| ppwd.pop(-1) | ||
| current_name,current_type = ppwd[-1] | ||
| _,bottom_folder = os.path.split(self.cwd) | ||
| mod_names = list([name for name,_ in self.ppwd]) | ||
| for i,(package_name,_) in enumerate(self.ppwd): | ||
| if bottom_folder == package_name: | ||
| mod_names = [name for name,_ in self.ppwd[i+1:]] | ||
| break | ||
| return os.path.abspath(os.path.join(self.cwd,*mod_names)) | ||
| def do_editvim(self,args): | ||
@@ -323,7 +408,7 @@ """ | ||
| editor=['vim'] | ||
| target_fqn = '.'.join(item[0] for item in self.pwd) | ||
| target_fqn = '.'.join(item[0] for item in self.ppwd) | ||
| if target_fqn != '': | ||
| current_name,current_type = self.pwd[-1] | ||
| current_name,current_type = self.ppwd[-1] | ||
| if current_type == 'package': | ||
| filepath = os.path.abspath(os.path.join(self.cwd,*target_fqn.split('.')) + '/__init__.py') | ||
| filepath = self._get_path() + '__init__.py' | ||
| if os.path.getsize(filepath) == 0: | ||
@@ -335,3 +420,11 @@ print('File is empty') | ||
| elif current_type == 'module': | ||
| filepath = os.path.abspath(os.path.join(self.cwd,*target_fqn.split('.'))+'.py') | ||
| _,bottom_folder = os.path.split(self.cwd) | ||
| mod_ppwd = list(self.ppwd) | ||
| for i,(package_name,_) in enumerate(self.ppwd): | ||
| if bottom_folder == package_name: | ||
| mod_ppwd = self.ppwd[i+1:] | ||
| break | ||
| filepath = self._get_path() + '.py' | ||
| if os.path.getsize(filepath) == 0: | ||
@@ -345,3 +438,3 @@ print('File is empty') | ||
| obj,mod,mod_fqn = get_target(target_fqn) | ||
| ast_obj,filepath,source = get_ast_obj(target_fqn,obj,mod,mod_fqn) | ||
| ast_obj,filepath,source,src_lines = get_ast_obj(target_fqn,obj,mod,mod_fqn) | ||
| lineno = ast_obj.lineno | ||
@@ -365,3 +458,3 @@ #except: | ||
| """ | ||
| target_fqn = '.'.join(item[0] for item in self.pwd) | ||
| target_fqn = '.'.join(item[0] for item in self.ppwd) | ||
| if target_fqn != '': | ||
@@ -374,4 +467,4 @@ try: | ||
| args = args.strip() | ||
| obj_type = self.pwd[-1][1] | ||
| if len(self.pwd) == 0: | ||
| obj_type = self.ppwd[-1][1] | ||
| if len(self.ppwd) == 0: | ||
| print('No target is selected') | ||
@@ -401,5 +494,5 @@ return | ||
| def do_listdir(self,args): | ||
| def do_ls(self,args): | ||
| """ | ||
| Help: (devshell)$ listdir path | ||
| Help: (devshell)$ ls path | ||
| This lists the files/subfolders within the provided operating system folder path | ||
@@ -428,12 +521,16 @@ If path is not provided, then files/subfolders within the operating system folder path (current working directory) will be listed | ||
| def do_chdir(self,args): | ||
| def do_cd(self,args): | ||
| """ | ||
| Help: (devshell)$ chdir path | ||
| Help: (devshell)$ cd path | ||
| This changes the operating system folder path (current working directory) where devshell will look for packages and modules | ||
| """ | ||
| if os.path.exists(args) and os.path.isdir(args): | ||
| if os.path.exists(os.path.join(args,'__init__.py')): | ||
| self.do_pcd(args) | ||
| else: | ||
| self.ppwd = [] | ||
| self._pls_cache = None | ||
| os.chdir(args) | ||
| self.cwd = os.getcwd() | ||
| self.pwd = [] | ||
| self._ls_cache = None | ||
| sys.path = self.orig_sys_path + [self.cwd] | ||
| else: | ||
@@ -445,9 +542,13 @@ print('Error - path does not exist: %s' % args) | ||
| Help: (devshell)$ doctestify | ||
| (devshell)$ doctestify resume | ||
| Performs doctestify on the currently targeted item. | ||
| This will cause an interactive python recording session to begin with all items from the targeted item's module imported in automatically. | ||
| All inputs and outputs will be recorded and entered into the targeted item's docstring as a doctest. | ||
| If "doctestify resume" is called, then the current doctest commands will be automatically executed into the interpreter | ||
| """ | ||
| target_fqn = '.'.join(item[0] for item in self.pwd) | ||
| target_fqn = '.'.join(item[0] for item in self.ppwd) | ||
| if target_fqn != '': | ||
| doctestify(target_fqn) | ||
| resume = args.strip() == 'resume' | ||
| doctestify(target_fqn,resume) | ||
| else: | ||
@@ -487,7 +588,7 @@ print('No target identified') | ||
| """ | ||
| if len(self.pwd) == 0: | ||
| if len(self.ppwd) == 0: | ||
| print('No target identified') | ||
| return | ||
| current_type = self.pwd[-1][1] | ||
| target_fqn = '.'.join(item[0] for item in self.pwd) | ||
| current_type = self.ppwd[-1][1] | ||
| target_fqn = '.'.join(item[0] for item in self.ppwd) | ||
| if target_fqn != '': | ||
@@ -543,3 +644,3 @@ try: | ||
| If there is no currently targeted item, pytest will be run against the folder indicated by getcwd: | ||
| If there is no currently targeted item, pytest will be run against the folder indicated by pwd: | ||
| (devshell)$ pytest -ra --doctest-modules | ||
@@ -563,6 +664,6 @@ is equivalent to: | ||
| arglist = [arg.strip() for arg in args.split() if arg.strip() != ''] | ||
| if len(self.pwd) == 0: | ||
| if len(self.ppwd) == 0: | ||
| run_cmd([sys.executable,'-m','pytest',os.path.abspath(self.cwd),'--doctest-modules']+arglist) | ||
| return | ||
| current_type = self.pwd[-1][1] | ||
| current_type = self.ppwd[-1][1] | ||
@@ -572,3 +673,3 @@ item_names = [] | ||
| item_names_inside_module = [] | ||
| for item_name,item_type in self.pwd: | ||
| for item_name,item_type in self.ppwd: | ||
| item_names.append(item_name) | ||
@@ -609,3 +710,3 @@ if reached_module: | ||
| If there is no currently targeted item, coverage and pytest will be run against the folder indicated by getcwd: | ||
| If there is no currently targeted item, coverage and pytest will be run against the folder indicated by pwd: | ||
| (devshell)$ coverage -ra | ||
@@ -634,10 +735,10 @@ is functionally equivalent to: | ||
| arglist = [arg.strip() for arg in args.split() if arg.strip() != ''] | ||
| if len(self.pwd) == 0: | ||
| if len(self.ppwd) == 0: | ||
| run_coverage(self.cwd,arglist) | ||
| return | ||
| current_type = self.pwd[-1][1] | ||
| current_type = self.ppwd[-1][1] | ||
| item_names = [] | ||
| reached_module = False | ||
| item_names_inside_module = [] | ||
| for item_name,item_type in self.pwd: | ||
| for item_name,item_type in self.ppwd: | ||
| item_names.append(item_name) | ||
@@ -668,7 +769,7 @@ if reached_module: | ||
| """ | ||
| target_fqn = '.'.join(item[0] for item in self.pwd) | ||
| target_fqn = '.'.join(item[0] for item in self.ppwd) | ||
| if target_fqn != '': | ||
| current_name,current_type = self.pwd[-1] | ||
| current_name,current_type = self.ppwd[-1] | ||
| if current_type == 'package': | ||
| filepath = os.path.abspath(os.path.join(self.cwd,*target_fqn.split('.')) + '/__init__.py') | ||
| filepath = self._get_path() + '/__init__.py' | ||
| if os.path.getsize(filepath) == 0: | ||
@@ -680,3 +781,3 @@ print('File is empty') | ||
| elif current_type == 'module': | ||
| filepath = os.path.abspath(os.path.join(self.cwd,*target_fqn.split('.'))+'.py') | ||
| filepath = self._get_path() + '.py' | ||
| if os.path.getsize(filepath) == 0: | ||
@@ -726,12 +827,12 @@ print('File is empty') | ||
| """ | ||
| target_fqn = '.'.join(item[0] for item in self.pwd) | ||
| target_fqn = '.'.join(item[0] for item in self.ppwd) | ||
| if target_fqn != '': | ||
| current_name,current_type = self.pwd[-1] | ||
| current_name,current_type = self.ppwd[-1] | ||
| if current_type == 'package': | ||
| #recurse through package directory | ||
| path = os.path.abspath(os.path.join(self.cwd,*target_fqn.split('.'))) | ||
| path = self._get_path() | ||
| grep(args,path=path) | ||
| elif current_type == 'module': | ||
| filepath = os.path.abspath(os.path.join(self.cwd,*target_fqn.split('.'))+'.py') | ||
| filepath = self._get_path() + '.py' | ||
| grep(args,path=filepath) | ||
@@ -759,3 +860,3 @@ else: | ||
| """ | ||
| target_fqn = '.'.join(item[0] for item in self.pwd) | ||
| target_fqn = '.'.join(item[0] for item in self.ppwd) | ||
| if target_fqn != '': | ||
@@ -769,6 +870,6 @@ try: | ||
| lines = [] | ||
| if self.pwd[-1][1] in self._callable: | ||
| lines.append(self.pwd[-1][1]+': '+self.pwd[-1][0] + str(inspect.signature(obj))) | ||
| if self.ppwd[-1][1] in self._callable: | ||
| lines.append(self.ppwd[-1][1]+': '+self.ppwd[-1][0] + str(inspect.signature(obj))) | ||
| else: | ||
| lines.append(self.pwd[-1][1]+': '+self.pwd[-1][0]) | ||
| lines.append(self.ppwd[-1][1]+': '+self.ppwd[-1][0]) | ||
| if doc is not None: | ||
@@ -790,11 +891,11 @@ lines.append('"""') | ||
| def do_getcwd(self,args): | ||
| def do_pwd(self,args): | ||
| """ | ||
| Help: (devshell)$ getcwd | ||
| Help: (devshell)$ pwd | ||
| This displays the operating system folder path (current working directory) where devshell will look for packages and modules | ||
| """ | ||
| print(self.cwd) | ||
| def do_ls(self,args): | ||
| def do_pls(self,args): | ||
| """ | ||
| Help: (devshell)$ ls [python_object] | ||
| Help: (devshell)$ pls [python_object] | ||
| This will show all items contained within the currently targeted item. | ||
@@ -810,7 +911,5 @@ e.g. for a package, this would list the modules | ||
| This is NOT the same as the usual interpretation of ls in other shells. | ||
| For the usual interpretation, see listdir. | ||
| """ | ||
| lines = [] | ||
| result = self._ls(args) | ||
| result = self._pls(args) | ||
| if result is None: | ||
@@ -820,3 +919,3 @@ return | ||
| if item_type in self._cdable: | ||
| if len(self.pwd) == 0 and item_type == 'package' and not os.path.exists(os.path.join(self.cwd,item_name,'__init__.py')) and os.path.exists(os.path.join(self.cwd,'src',item_name,'__init__.py')): | ||
| if len(self.ppwd) == 0 and item_type == 'package' and not os.path.exists(os.path.join(self.cwd,item_name,'__init__.py')) and os.path.exists(os.path.join(self.cwd,'src',item_name,'__init__.py')): | ||
| lines.append(' %s%sdirectory (./src)' % (item_name.ljust(30), item_type.ljust(30))) | ||
@@ -829,64 +928,62 @@ else: | ||
| print('\n'.join(lines)) | ||
| def _pwd(self): | ||
| if len(self.pwd) > 0: | ||
| return ('/'+'.'.join(item[0] for item in self.pwd),self.pwd[-1][1]) | ||
| def _ppwd(self): | ||
| if len(self.ppwd) > 0: | ||
| return ('/'+'.'.join(item[0] for item in self.ppwd),self.ppwd[-1][1]) | ||
| else: | ||
| return '/','root' | ||
| def do_pwd(self,args): | ||
| def do_ppwd(self,args): | ||
| """ | ||
| Help: (devshell)$ pwd | ||
| Help: (devshell)$ ppwd | ||
| This shows the fully qualified name of the currently targeted item. | ||
| This is NOT the same as the usual interpretation of pwd in other shells. | ||
| For the usual interpretation, see getcwd. | ||
| """ | ||
| pwd,current_type = self._pwd() | ||
| print('%s (%s)' % (pwd.ljust(30),current_type)) | ||
| ppwd,current_type = self._ppwd() | ||
| print('%s (%s)' % (ppwd.ljust(30),current_type)) | ||
| def _cd(self,args): | ||
| def _pcd(self,args): | ||
| resolved = False | ||
| clear_ls_cache = False | ||
| clear_pls_cache = False | ||
| if args == '.': | ||
| resolved = True | ||
| clear_ls_cache = False | ||
| clear_pls_cache = False | ||
| elif args == '..': | ||
| if len(self.pwd) > 0: | ||
| last_item,last_item_type = self.pwd.pop() | ||
| if len(self.pwd) == 0 and last_item_type == 'package' and os.path.basename(self.cwd) == 'src' and os.path.exists(os.path.join(self.cwd,last_item,'__init__.py')) and not os.path.exists(os.path.join(self.cwd,'..',last_item,'__init__.py')): | ||
| self.do_chdir('..') | ||
| if len(self.ppwd) > 0: | ||
| last_item,last_item_type = self.ppwd.pop() | ||
| if len(self.ppwd) == 0 and last_item_type == 'package' and os.path.basename(self.cwd) == 'src' and os.path.exists(os.path.join(self.cwd,last_item,'__init__.py')) and not os.path.exists(os.path.join(self.cwd,'..',last_item,'__init__.py')): | ||
| self.do_cd('..') | ||
| resolved = True | ||
| clear_ls_cache = True | ||
| clear_pls_cache = True | ||
| #go up if in src | ||
| elif args == '/': | ||
| del self.pwd[:] | ||
| del self.ppwd[:] | ||
| resolved = True | ||
| clear_ls_cache = True | ||
| clear_pls_cache = True | ||
| elif '.' not in args: | ||
| for item,item_type in self._ls(): | ||
| for item,item_type in self._pls(): | ||
| if item == args: | ||
| if len(self.pwd) == 0 and item_type == 'package' and not os.path.exists(os.path.join(self.cwd,item,'__init__.py')) and os.path.exists(os.path.join(self.cwd,'src',item,'__init__.py')): | ||
| self.do_chdir('src') | ||
| self.pwd.append((item,item_type)) | ||
| if len(self.ppwd) == 0 and item_type == 'package' and not os.path.exists(os.path.join(self.cwd,item,'__init__.py')) and os.path.exists(os.path.join(self.cwd,'src',item,'__init__.py')): | ||
| self.do_cd('src') | ||
| self.ppwd.append((item,item_type)) | ||
| resolved = True | ||
| clear_ls_cache = True | ||
| clear_pls_cache = True | ||
| break | ||
| else: | ||
| pieces = args.split('.') | ||
| orig_pwd = list(self.pwd) | ||
| orig_ppwd = list(self.ppwd) | ||
| resolved = True | ||
| clear_ls_cache = False | ||
| orig_ls_cache = self._ls_cache | ||
| clear_pls_cache = False | ||
| orig_pls_cache = self._pls_cache | ||
| for piece in pieces: | ||
| self._ls_cache = None | ||
| piece_resolved,piece_clear_ls_cache = self._cd(piece) | ||
| self._pls_cache = None | ||
| piece_resolved,piece_clear_pls_cache = self._pcd(piece) | ||
| if not piece_resolved: | ||
| resolved = False | ||
| self.pwd = orig_pwd | ||
| clear_ls_cache = False | ||
| self.ppwd = orig_ppwd | ||
| clear_pls_cache = False | ||
| break | ||
| else: | ||
| clear_ls_cache = clear_ls_cache or piece_clear_ls_cache | ||
| self._ls_cache = orig_ls_cache | ||
| return (resolved,clear_ls_cache) | ||
| clear_pls_cache = clear_pls_cache or piece_clear_pls_cache | ||
| self._pls_cache = orig_pls_cache | ||
| return (resolved,clear_pls_cache) | ||
| def do_interactive(self,args): | ||
@@ -906,5 +1003,5 @@ """ | ||
| def do_cd(self,args): | ||
| def do_pcd(self,args): | ||
| """ | ||
| Help: (devshell)$ cd <argument> | ||
| Help: (devshell)$ pcd <argument> | ||
| This changes the currently targeted item. | ||
@@ -914,3 +1011,3 @@ | ||
| If there is no current target, then one may cd into a package within the current working directory or within a package in a subfolder of the current working directory named "src". | ||
| If there is no current target, then one may pcd into a package within the current working directory or within a package in a subfolder of the current working directory named "src". | ||
| Cding into the "src" subfolder only occurs when the src subfolder has the package with the given name and the current working directory does not. | ||
@@ -923,31 +1020,29 @@ Cding into the "src" subfolder will change the current working directory to be the "src" subfolder. | ||
| (devshell)$ cd / | ||
| (devshell)$ pcd / | ||
| This will remove all parts of the current fully qualified name | ||
| (devshell)$ cd . | ||
| (devshell)$ pcd . | ||
| This has no effect | ||
| (devshell)$ cd .. | ||
| (devshell)$ pcd .. | ||
| This removes the last piece of the currently fully qualified name (navigates up to the parent item) | ||
| If leaving a package to a subfolder named "src", will also change the current working directory to be the parent directory of "src" if a package with the current target as its name exists only in the "src" directory and not in the parent directory. | ||
| This is NOT the same as the usual interpretation of cd in other shells. | ||
| For the usual interpretation, see chdir. | ||
| """ | ||
| resolved,clear_ls_cache = self._cd(args) | ||
| resolved,clear_pls_cache = self._pcd(args) | ||
| if not resolved is True: | ||
| print('Error - "%s" does not exist' % args) | ||
| #else: | ||
| # self.prompt = '(devshell)%s$ ' % self._pwd() | ||
| if clear_ls_cache: | ||
| self._ls_cache = None | ||
| def complete_cd(self,text,line,begin_idx,end_idx): | ||
| # self.prompt = '(devshell)%s$ ' % self._ppwd() | ||
| if clear_pls_cache: | ||
| self._pls_cache = None | ||
| def complete_pcd(self,text,line,begin_idx,end_idx): | ||
| return self._complete_python(text,line,begin_idx,end_idx) | ||
| def complete_ls(self,text,line,begin_idx,end_idx): | ||
| def complete_pls(self,text,line,begin_idx,end_idx): | ||
| return self._complete_python(text,line,begin_idx,end_idx) | ||
| def _complete_python(self,text,line,begin_idx,end_idx): | ||
| orig_cwd = self.cwd | ||
| last_piece = shlex.split(text)[-1] | ||
| if '.' not in text: | ||
| results = [item[0] for item in self._ls() if item[0].startswith(text)] | ||
| results = [item[0] for item in self._pls() if item[0].startswith(last_piece)] | ||
| if self.cwd != orig_cwd: | ||
@@ -958,17 +1053,21 @@ self.cwd = orig_cwd | ||
| elif set(last_piece) == set(['.']): | ||
| return [] | ||
| else: | ||
| orig_pwd = list(self.pwd) | ||
| orig_ls_cache = self._ls_cache | ||
| orig_ppwd = list(self.ppwd) | ||
| orig_pls_cache = self._pls_cache | ||
| ts = text.split('.') | ||
| front = '.'.join(ts[:-1]) | ||
| #front = '.'.join(ts[:-1]) | ||
| front = shlex.join(shlex.split('.'.join(ts[:-1]))[1:]) | ||
| last_piece = ts[-1] | ||
| resolved,clear_ls_cache = self._cd(front) | ||
| resolved,clear_pls_cache = self._pcd(front) | ||
| #resolved,clear_pls_cache = self._pcd(shlex.join(shlex.split(front)[1:])) | ||
| if resolved: | ||
| self._ls_cache = None | ||
| results = [front+'.'+item[0] for item in self._ls() if item[0].startswith(last_piece)] | ||
| self._pls_cache = None | ||
| results = [front+'.'+item[0] for item in self._pls() if item[0].startswith(last_piece)] | ||
| else: | ||
| results = [] | ||
| self.pwd = orig_pwd | ||
| self._ls_cache = orig_ls_cache | ||
| self.ppwd = orig_ppwd | ||
| self._pls_cache = orig_pls_cache | ||
| if self.cwd != orig_cwd: | ||
@@ -978,7 +1077,7 @@ self.cwd = orig_cwd | ||
| return results | ||
| def complete_chdir(self,text,line,begin_idx,end_idx): | ||
| return self._complete_dirs('chdir',text,line,begin_idx,end_idx) | ||
| def complete_cd(self,text,line,begin_idx,end_idx): | ||
| return self._complete_dirs('cd',text,line,begin_idx,end_idx) | ||
| def complete_listdir(self,text,line,begin_idx,end_idx): | ||
| return self._complete_dirs('listdir',text,line,begin_idx,end_idx) | ||
| def complete_ls(self,text,line,begin_idx,end_idx): | ||
| return self._complete_dirs('ls',text,line,begin_idx,end_idx) | ||
@@ -1014,3 +1113,3 @@ def complete_rmtree(self,text,line,begin_idx,end_idx): | ||
| last_piece = path | ||
| return [item for item in os.listdir(front) if item.startswith(last_piece) and os.path.isdir(os.path.join(front,item))] | ||
| return [(os.path.join(front,item) if front != '.' else item) for item in os.listdir(front) if item.startswith(last_piece) and os.path.isdir(os.path.join(front,item))] | ||
@@ -1029,3 +1128,3 @@ def _complete_files(self,cmd,text,line,begin_idx,end_idx): | ||
| last_piece = path | ||
| return [item for item in os.listdir(front) if item.startswith(last_piece) and not os.path.isdir(os.path.join(front,item))] | ||
| return [(os.path.join(front,item) if front != '.' else item) for item in os.listdir(front) if item.startswith(last_piece) and not os.path.isdir(os.path.join(front,item))] | ||
| def _complete_lastdirfile(self,cmd,text,line,begin_idx,end_idx): | ||
@@ -1032,0 +1131,0 @@ if cmd is not None: |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
352232
337.26%22
69.23%1448
13.84%