typin’s documentation¶
typin is a Type Inferencer for understanding what types of objects
are flowing through your Python code. It observes your code dynamically and can
record all the types that each function sees, returns or raises.
typin can then use this information to create Python’s type annotations or
__doc__ strings to insert into your code.
Contents:
typin README¶
typin is a Type Inferencer for understanding what types of objects
are flowing through your Python code. It observes your code dynamically and can
record all the types that each function sees, returns or raises.
typin can then use this information to create Python’s type annotations or
__doc__ strings to insert into your code.
typin is currently proof-of-concept and a very early prototype.
It is Python 3 only at the moment.
There is a forthcoming project https://github.com/paulross/pytest-typin which
turns typin into a pytest plugin so that your unit tests can generate type
annotations and documentation strings.
Example¶
Lets say you have a function that creates a repeated string, like this:
def function(s, num):
if num < 1:
raise ValueError('Value must be > 0, not {:d}'.format(num))
lst = []
while num:
lst.append(s)
num -= 1
return ' '.join(lst)
You can exercise this under the watchful gaze of typin:
from typin import type_inferencer
with type_inferencer.TypeInferencer() as ti:
assert function('Hi', 2) == 'Hi Hi'
You can then get the types that typin has observed as a string suitable for
a stub file:
ti.stub_file_str(__file__, '', 'function')
# returns: 'def function(s: str, num: int) -> str: ...'
Then adding code that provokes the exception we can track that as well:
from typin import type_inferencer
with type_inferencer.TypeInferencer() as ti:
assert function('Hi', 2) == 'Hi Hi' # As before
try:
function('Hi', 0)
except ValueError:
pass
Exception specifications are not part of Python’s type annotation but they are
part of of the Sphinx documentation string standard and typin can provide that, and
the line number where it should be inserted:
line_number, docstring = ti.docstring(__file__, '', 'function', style='sphinx')
docstring
"""
<insert documentation for function>
:param s: <insert documentation for argument>
:type s: ``str``
:param num: <insert documentation for argument>
:type num: ``int``
:returns: ``str`` -- <insert documentation for return values>
:raises: ``ValueError``
"""
# Insert template docstrings into the source code.
new_src = ti.insert_docstrings(__file__, style='sphinx')
with open(__file__, 'w') as f:
for line in new_src:
f.write(line)
Sadly typin is not smart enough to write the documentation text for you :-)
There is a CLI interface typin_cli that is an entry point to typin/src/typin/typin_cli.py.
This executes arbitrary python code using compile() and exec() like the following example.
Note use of -- followed by Python script then the arguments for that script surrounded by quotes:
$ python typin_cli.py --stubs=stubs/ --write-docstrings=docstrings/ -- example.py 'foo bar baz'
This will compile()/exec() example.py with the arguments foo bar baz
write the stub files ('.pyi' files) to stubs/ and the source code with the docstrings
inserted to docstrings/.
typin_cli.py help:
$ python typin_cli.py --help
usage: typin_cli.py [-h] [-l LOGLEVEL] [-d] [-t] [-e EVENTS_TO_TRACE]
[-s STUBS] [-w WRITE_DOCSTRINGS]
[--docstring-style DOCSTRING_STYLE] [-r ROOT]
program argstring
typin_cli - Infer types of Python functions.
Created by Paul Ross on 2017-10-25. Copyright 2017. All rights reserved.
Version: v0.1.0 Licensed under MIT License
USAGE
positional arguments:
program Python target file to be compiled and executed.
argstring Argument as a string to give to the target. Prefix
this with '--' to avoid them getting consumed by
typin_cli.py
optional arguments:
-h, --help show this help message and exit
-l LOGLEVEL, --loglevel LOGLEVEL
Log Level (debug=10, info=20, warning=30, error=40,
critical=50) [default: 30]
-d, --dump Dump results on stdout after processing. [default:
False]
-t, --trace-frame-events
Very verbose trace output, one line per frame event.
[default: False]
-e EVENTS_TO_TRACE, --events-to-trace EVENTS_TO_TRACE
Events to trace (additive). [default: []] i.e. every
event.
-s STUBS, --stubs STUBS
Directory to write stubs files. [default: ]
-w WRITE_DOCSTRINGS, --write-docstrings WRITE_DOCSTRINGS
Directory to write source code with docstrings.
[default: ]
--docstring-style DOCSTRING_STYLE
Style of docstrings, can be: 'google', 'sphinx'.
[default: sphinx]
-r ROOT, --root ROOT Root path of the Python packages to generate stub
files for. [default: .]
Python type inferencing.
- Free software: MIT license
- Documentation: https://typin.readthedocs.io.
Features¶
- TODO
Credits¶
This package was created with Cookiecutter and the audreyr/cookiecutter-pypackage project template.
Installation¶
First make a virtual environment in your <PYTHONVENVS>, say ~/pyvenvs:
$ python3 -m venv <PYTHONVENVS>/typin
$ . <PYTHONVENVS>/typin/bin/activate
(typin) $
Stable release¶
To install typin, run this command in your terminal:
$ pip install typin
This is the preferred method to install typin, as it will always install the most recent stable release.
If you don’t have pip installed, this Python installation guide can guide you through the process.
From sources¶
The sources for typin can be downloaded from the Github repo.
You can either clone the public repository:
$ git clone git://github.com/paulross/typin
Or download the tarball:
$ curl -OL https://github.com/paulross/typin/tarball/master
Once you have a copy of the source, you can install it with:
$ python setup.py install
Install the test dependencies and run typin’s tests:
(typin) $ pip install pytest
(typin) $ pip install pytest-runner
(typin) $ python setup.py test
Developing with typin¶
If you are developing with typin you need test coverage and documentation tools.
Test Coverage¶
Install pytest-cov:
(typin) $ pip install pytest-cov
The most meaningful invocation that elimates the top level tools is:
(typin) $ pytest --cov=typin --cov-report html tests/
Documentation¶
If you want to build the documentation you need to:
(typin) $ pip install Sphinx
(typin) $ cd docs
(typin) $ make html
The landing page is docs/_build/html/index.html.
Example¶
There is a CLI interface typin/src/typin/typin_cli.py to execute arbitrary
python code using compile() and exec() like this:
python typin_cli.py --stubs=stubs -- example.py 'foo bar baz'
This will compile()/exec() example.py with the arguments foo bar baz
and dump out the results. These include the docstrings for the functions in example.py which
have been inserted in that source code to produce this:
example.py¶
-
class
typin.example.ExampleClass(first_name, last_name)[source]¶ An example class with a couple of methods that we exercise.
-
class
typin.example.InnerClass(value)[source]¶ Same named inner class to explore inner/outer namespaces.
-
class
typin.example.MyNT(a, b, c)¶ -
a¶ Alias for field number 0
-
b¶ Alias for field number 1
-
c¶ Alias for field number 2
-
-
class
typin.example.OuterClass(value)[source]¶ Example outer class to explore inner/outer issues.
Reference¶
typin_cli¶
types¶
Created on 17 Jul 2017
@author: paulross
-
class
typin.types.FunctionTypes(signature=None)[source]¶ Class that accumulate function call data such as call arguments, return values and exceptions raised.
-
add_return(return_value, line_number)[source]¶ Records a return value at a particular line number. If the return_value is None and we have previously seen an exception at this line then this is a phantom return value and must be ignored. See
TypeInferencer.__enter__for a description of this.
-
argument_type_strings¶ A
collections.OrderedDictof{argument_name : set(types, ...), ...}where the types are strings.
-
docstring(include_returns, style='sphinx')[source]¶ Returns a pair (line_number, docstring) for this function. The docstring is the __doc__ for the function and the line_number is the docstring position (function declaration + 1). So to insert into a list of lines called
src:src[:line_number] + docstring.split('\n') + src[line_number:]
style can be: ‘sphinx’, ‘google’.
Raises: TypesExceptionBaseor derived class.
-
exception_type_strings¶ A dict of
{line_number : set(types, ...), ...}for any exceptions raised where the return types are strings. There should only be one type in the set.
-
filtered_arguments()[source]¶ A
collections.OrderedDictof{argument_name : set(types, ...), ...}where the types are strings. This removes the ‘self’ argument if it is the first argument.
-
line_decl¶ Line number of the function declaration as an integer.
Returns: int– Function declaration line.Raises: FunctionTypesExceptionNoDataIf there is no entry points recorded.
-
line_range¶ A pair of line numbers of the span of the function as integers. The first is the declaration of the function, the last is the extreme return point or exception.
-
num_entry_points¶ The number of entry points, 1 for normal functions >1 for generators. 0 Something wrong.
-
return_type_strings¶ A dict of
{line_number : set(types, ...), ...}for the return values where the return types are strings. There should only be one type in the set.
-
-
exception
typin.types.FunctionTypesExceptionNoData[source]¶ Exception thrown when no call date has been added to a FunctionTypes object.
type_inferencer¶
Contributing¶
Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.
You can contribute in many ways:
Types of Contributions¶
Report Bugs¶
Report bugs at https://github.com/paulross/typin/issues.
If you are reporting a bug, please include:
- Your operating system name and version.
- Any details about your local setup that might be helpful in troubleshooting.
- Detailed steps to reproduce the bug.
Fix Bugs¶
Look through the GitHub issues for bugs. Anything tagged with “bug” and “help wanted” is open to whoever wants to implement it.
Implement Features¶
Look through the GitHub issues for features. Anything tagged with “enhancement” and “help wanted” is open to whoever wants to implement it.
Write Documentation¶
typin could always use more documentation, whether as part of the official typin docs, in docstrings, or even on the web in blog posts, articles, and such.
Submit Feedback¶
The best way to send feedback is to file an issue at https://github.com/paulross/typin/issues.
If you are proposing a feature:
- Explain in detail how it would work.
- Keep the scope as narrow as possible, to make it easier to implement.
- Remember that this is a volunteer-driven project, and that contributions are welcome :)
Get Started!¶
Ready to contribute? Here’s how to set up typin for local development.
Fork the typin repo on GitHub.
Clone your fork locally:
$ git clone git@github.com:your_name_here/typin.git
Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:
$ mkvirtualenv typin $ cd typin/ $ python setup.py developCreate a branch for local development:
$ git checkout -b name-of-your-bugfix-or-feature
Now you can make your changes locally.
When you’re done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:
$ flake8 typin tests $ python setup.py test or py.test $ toxTo get flake8 and tox, just pip install them into your virtualenv.
Commit your changes and push your branch to GitHub:
$ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-featureSubmit a pull request through the GitHub website.
Pull Request Guidelines¶
Before you submit a pull request, check that it meets these guidelines:
- The pull request should include tests.
- If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst.
- The pull request should work for Python 2.6, 2.7, 3.3, 3.4 and 3.5, and for PyPy. Check https://travis-ci.org/paulross/typin/pull_requests and make sure that the tests pass for all supported Python versions.
Credits¶
Development Lead¶
- Paul Ross <apaulross@gmail.com>
Contributors¶
None yet. Why not be the first?
Various Research notes¶
Tracing functions¶
This applies to both 2.7 and 3.6.
sys.setprofile() only sees call and return. If an exception is raised the function appears to return None. sys.setprofile() can return None as the return value is ignored.
sys.settrace() is more fine grained and gets exception and line events as well. sys.settrace() can return itself, None or some other function and this will be respected.
Both are thread specific so it doesn’t make much sense to use these in a multithreaded environment.
Tracing Exceptions¶
If we have this code:
def a(arg): # Line 29
b('calling b()') # Line 30
return 'A' # Line 31
def b(arg): # Line 33
try:
c('calling c()') # Line 35
except ValueError:
pass
return 'B' # Line 38
def c(arg): # Line 40
raise ValueError() # Line 41
return 'C'
sys.settrace() sees the exception:
/Users/paulross/Documents/workspace/typin/src/typin/research.py 29 a call None
/Users/paulross/Documents/workspace/typin/src/typin/research.py 33 b call None
/Users/paulross/Documents/workspace/typin/src/typin/research.py 40 c call None
# c() raises. We can see this as an exception event is followed by a return None with the same lineno.
# Return None on its own is not enough as that might happen in the normal course of events.
/Users/paulross/Documents/workspace/typin/src/typin/research.py 41 c exception (<class 'ValueError'>, ValueError(), <traceback object at 0x102365c08>)
/Users/paulross/Documents/workspace/typin/src/typin/research.py 41 c return None
# b() reports the exception at the point that the call to c() is made.
# b() handles the exception, this can be detected by the exception and return events being on different lines.
/Users/paulross/Documents/workspace/typin/src/typin/research.py 35 b exception (<class 'ValueError'>, ValueError(), <traceback object at 0x102365c48>)
/Users/paulross/Documents/workspace/typin/src/typin/research.py 38 b return 'B'
/Users/paulross/Documents/workspace/typin/src/typin/research.py 31 a return 'A'
sys.setprofile() does not see the exception:
/Users/paulross/Documents/workspace/typin/src/typin/research.py 29 a call None
/Users/paulross/Documents/workspace/typin/src/typin/research.py 33 b call None
/Users/paulross/Documents/workspace/typin/src/typin/research.py 40 c call None
/Users/paulross/Documents/workspace/typin/src/typin/research.py 41 c return None
/Users/paulross/Documents/workspace/typin/src/typin/research.py 38 b return 'B'
/Users/paulross/Documents/workspace/typin/src/typin/research.py 31 a return 'A'
/Users/paulross/Documents/workspace/typin/src/typin/research.py 53 main c_call <built-in function setprofile>
I think at this stage that we should ignore exception specifications as static typing does not accomodate them interesting though they are. So we use sys.setprofile() for now.
2017-07-22¶
We neeed to use sys.settrace() because if the function ever raises then sys.setprofile() will only see it returning None which it might never actually do (implicitly or explicitly). So we would then record any function that raises as possibly returning None. With sys.settrace we can eliminate the false returns None by identifying, and ignoring, it as above.
2017-11-16 and 18¶
Raising and catching exceptions.
Given this code:
# This function is defined on line 45 def exception_propogates(): # Line 45
raise ValueError(‘Error message’) # Line 46 return ‘OK’
# This function is defined on line 50 def exception_caught(): # line 50
- try:
- raise ValueError(‘Bad value’) # line 52
- except ValueError as _err: # line 53
- pass
- try:
- raise KeyError(‘Bad key.’) # line 56
- except KeyError as _err: # line 57
- pass
return ‘OK’ # line 59
- try:
- exception_propogates()
- except ValueError:
- pass
exception_caught()
We get:
Event: research.py 45 exception_propogates call None Event: research.py 46 exception_propogates line None Event: research.py 46 exception_propogates exception (<class ‘ValueError’>, ValueError(‘Error message’,), <traceback object at 0x101031108>) Event: research.py 46 exception_propogates return None
And:
Event: research.py 49 exception_caught call None Event: research.py 50 exception_caught line None Event: research.py 51 exception_caught line None Event: research.py 51 exception_caught exception (<class ‘ValueError’>, ValueError(‘Bad value’,), <traceback object at 0x101a31188>) Event: research.py 54 exception_caught line None Event: research.py 55 exception_caught line None Event: research.py 56 exception_caught line None Event: research.py 57 exception_caught line None Event: research.py 57 exception_caught exception (<class ‘KeyError’>, KeyError(‘Bad key.’,), <traceback object at 0x101a310c8>) Event: research.py 60 exception_caught line None Event: research.py 61 exception_caught line None Event: research.py 62 exception_caught line None Event: research.py 62 exception_caught return ‘OK’
So exception propagation can be detected by the appearance of a return None at the same line number as the exception.
Caught exceptions have a line event following the exception where the line number is greater than that of the exception.
So when we see an exception event we need to defer judgement and wait until the next event to decide if it is propagated or not.
Exception propagates out of function:
Event: research.py 46 exception_propogates exception (<class ‘ValueError’>, ValueError(‘Error message’,), <traceback object at 0x101031108>) Event: research.py 46 exception_propogates return None
Exception does not propagate out of function:
Event: research.py 51 exception_caught exception (<class ‘ValueError’>, ValueError(‘Bad value’,), <traceback object at 0x101a31188>) Event: research.py 54 exception_caught line None
So if the event following the exception is the same line number, event == ‘return’ and arg (return value) is None then ignore the return value and record the exception.
If the next event is event == line event at a line greater than the Exception event then the exception has been caught internally.
In both cases the event following the exception must have the same file and function and the arg must be None.
2017-11-17¶
sys.settrace() and sys.setprofile():
sys.settrace() creates ‘call’, ‘line’, ‘return’, ‘exception’ events. sys.setprofile() creates ‘call’, ‘c_call’, ‘return’, ‘c_return’, ‘exception’ events.
Both sys.settrace() and sys.setprofile() can be set to the same function but then you get duplicates:
Event: research.py 30 func_a call None Event: research.py 30 func_a call None Event: research.py 31 func_a line None Event: research.py 34 func_b call None Event: research.py 34 func_b call None Event: research.py 35 func_b line None Event: research.py 36 func_b line None Event: research.py 41 func_c call None Event: research.py 41 func_c call None Event: research.py 42 func_c line None Event: research.py 42 func_c exception (<class ‘ValueError’>, ValueError(), <traceback object at 0x1022150c8>) Event: research.py 42 func_c return None Event: research.py 42 func_c return None
2017-11-20¶
Revisiting order of events with exceptions:
(typin_00) Pauls-MacBook-Pro-2:typin paulross$ python research.py Event: research.py 81 func_that_catches_import call None Event: research.py 82 func_that_catches_import line None Event: research.py 83 func_that_catches_import line None Event: /Users/paulross/Documents/workspace/typin/src/typin/research_import.py 5 func_no_catch call None Event: /Users/paulross/Documents/workspace/typin/src/typin/research_import.py 6 func_no_catch line None Event: /Users/paulross/Documents/workspace/typin/src/typin/research_import.py 2 func_that_raises call None Event: /Users/paulross/Documents/workspace/typin/src/typin/research_import.py 3 func_that_raises line None Event: /Users/paulross/Documents/workspace/typin/src/typin/research_import.py 3 func_that_raises exception (<class ‘ValueError’>, ValueError(‘Error message’,), <traceback object at 0x102333348>) Event: /Users/paulross/Documents/workspace/typin/src/typin/research_import.py 3 func_that_raises return None Event: /Users/paulross/Documents/workspace/typin/src/typin/research_import.py 6 func_no_catch exception (<class ‘ValueError’>, ValueError(‘Error message’,), <traceback object at 0x1023332c8>) Event: /Users/paulross/Documents/workspace/typin/src/typin/research_import.py 6 func_no_catch return None Event: research.py 83 func_that_catches_import exception (<class ‘ValueError’>, ValueError(‘Error message’,), <traceback object at 0x1023333c8>) Event: research.py 84 func_that_catches_import line None Event: research.py 85 func_that_catches_import line None Event: research.py 85 func_that_catches_import return None (typin_00) Pauls-MacBook-Pro-2:typin paulross$
Simplifying file names:
(typin_00) Pauls-MacBook-Pro-2:typin paulross$ python research.py Event: research.py 81 func_that_catches_import call None Event: research.py 82 func_that_catches_import line None Event: research.py 83 func_that_catches_import line None Event: research_import.py 5 func_no_catch call None Event: research_import.py 6 func_no_catch line None Event: research_import.py 2 func_that_raises call None Event: research_import.py 3 func_that_raises line None Event: research_import.py 3 func_that_raises exception (<class ‘ValueError’>, ValueError(‘Error message’,), <traceback object at 0x102333348>) Event: research_import.py 3 func_that_raises return None Event: research_import.py 6 func_no_catch exception (<class ‘ValueError’>, ValueError(‘Error message’,), <traceback object at 0x1023332c8>) Event: research_import.py 6 func_no_catch return None Event: research.py 83 func_that_catches_import exception (<class ‘ValueError’>, ValueError(‘Error message’,), <traceback object at 0x1023333c8>) Event: research.py 84 func_that_catches_import line None Event: research.py 85 func_that_catches_import line None Event: research.py 85 func_that_catches_import return None (typin_00) Pauls-MacBook-Pro-2:typin paulross$
Exception raised, not caught:
Event: research_import.py 3 func_that_raises exception (<class ‘ValueError’>, ValueError(‘Error message’,), <traceback object at 0x102333348>) Event: research_import.py 3 func_that_raises return None
self.exception_in_progress is created with:
filename: research_import.py function: func_that_raises lineno: 3 exception_value: ValueError(‘Error message’,) eventno: X
Next event has same filename, function, lineno, returning None with event X+1 and this means the exception is propogated. So add the exception to the func_types.add_exception and set self.exception_in_progress to None.
Exception propogated: Event: research_import.py 6 func_no_catch exception (<class ‘ValueError’>, ValueError(‘Error message’,), <traceback object at 0x1023332c8>) Event: research_import.py 6 func_no_catch return None
self.exception_in_progress is created with:
filename: research_import.py function: func_no_catch lineno: 6 exception_value: ValueError(‘Error message’,) eventno: X
This is as above. Next event has same filename, function, lineno, returning None with event X+1 and this means the exception is propogated. So add the exception to the func_types.add_exception and set self.exception_in_progress to None.
Event: research.py 83 func_that_catches_import exception (<class ‘ValueError’>, ValueError(‘Error message’,), <traceback object at 0x1023333c8>) Event: research.py 84 func_that_catches_import line None
self.exception_in_progress is created with:
filename: research.py function: func_that_catches_import lineno: 83 exception_value: ValueError(‘Error message’,) eventno: X
Next event is ‘line’ event has same filename, function with event X+1 and line > 83. This means the exception is caught. So do not add the exception to the func_types.add_exception but set self.exception_in_progress to None.
So the original analysis (above) is correct even when the exception is thrown across modules.