diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..fe6397a --- /dev/null +++ b/.coveragerc @@ -0,0 +1,6 @@ +[run] +omit = setup.py +include = typesafety/* + +[xml] +output = ./coverage.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eda71eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +__pycache__ +*.pyc + +# Automatically generated documentation +doc/source/typesafety_modules.rst + +# Build-related files +build/ +debian/files +debian/typesafety.debhelper.log +debian/typesafety.substvars +debian/typesafety/ +typesafety.egg-info/ + +# Coverage information +.coverage +coverage.* +htmlcov/ + +# Nose output +nosetests.xml + +# PyLint files +doc/reports/ +pylint_ext_import-graph.dot +pylint_import_graph.dot +pylint_int_import-graph.dot + +# tox directory +.tox diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..817c301 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,311 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=.git,doc, + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + + +[MESSAGES CONTROL] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +#disable= + +disable=C0111,W0611,R0201,W0613,R0903,E1101,I0011,W0232 + +# C0111 missing-docstring Our priority is to make the code document +# itself. Docstring is required only when +# the signature is not enough to tell +# everything important. +# +# W0611 unused-import Pylint does not count our type-hinting +# annotations as a use of a class name, thus, +# this rule results in many false-positives. +# +# R0201 no-self-use Extracting private methods inside a class +# to increase readability often results in +# some methods that don't use the `self`. +# +# W0613 unused-argument Methods of test doubles, for example stubs, +# often return canned values, not using any +# of their arguments. +# +# R0903 too-few-public-methods Node classes often have no methods at all. +# +# E1101 no-member Used when a variable is accessed for a +# nonexistent member. +# Reason: Too many false positives +# +# I0011 locally-disabled Disabling a Pylint warning is always the +# result of a decision, no additional notification +# is required to tell us what we did. +# +# W0232 no-init New-style classes (see above) do not require __init__ methods, +# the object class has a perfectly fine default constructor. + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +#output-format=parseable + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=3 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the beginning of the name of dummy variables +# (i.e. not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes= + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter,apply,input + +# Regular expression which should only match correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct module level names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression which should only match correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +# Private functions are allowed to have nice explanatory names +function-rgx=(([a-z][a-z0-9_]{2,40})|(__[a-z_0-9]{2,70}))$ + +# Regular expression which should only match correct method names +# Private methods and BDD-style xUnit test functions are allowed to have nice +# explanatory names +method-rgx=((_?[a-z][a-z0-9_]{2,40})|(__[a-z_0-9]{2,70})|(test[a-z0-9_]{2,100}))$ + +# Regular expression which should only match correct instance attribute names +# Private instance attribute names are allowed to have nice explanatory names +attr-rgx=((_?[a-z][a-z0-9_]{2,30})|(__[a-z_0-9]{2,70}))$ + +# Regular expression which should only match correct argument names +# Type annotated short argument names are okay +argument-rgx=[a-z_][a-z0-9_]{0,30}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{1,30}$ + +# Regular expression which should only match correct attribute names in class +# bodies +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_,f + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata,tmp,tmp2 + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=__.*__ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=120 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=0 + +# Maximum number of public methods for a class (see R0904). +# Classes inheriting from unittest.TestCase may have more than 50 methods +max-public-methods=100 + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph=pylint_import_graph.dot + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph=pylint_ext_import-graph.dot + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph=pylint_int_import-graph.dot + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..5f04e27 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,5 @@ +Viktor Hercinger +Attila M. Magyar +Laszlo Attila Toth +Imre Halasz +Medgyes Akos Adam diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4362b49 --- /dev/null +++ b/LICENSE @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6390e90 --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +PYTHON ?= python +NOSE = $(PYTHON) -m nose +PYLINT = $(PYTHON) -m pylint +PEP8 = $(PYTHON) -m pep8 + +TESTDIRS = typesafety/ + +.PHONY: pre-commit +pre-commit: codingstandards check + +.PHONY: pre-merge +pre-merge: pre-commit documentation + +.PHONY: check +check: + $(NOSE) --with-coverage --with-doctest -s $(TESTDIRS) + +.PHONY: codingstandards +codingstandards: check-copyright + $(PEP8) --repeat $(TESTDIRS) + $(PYLINT) -f parseable --rcfile=.pylintrc $(TESTDIRS) + +.PHONY: documentation +documentation: + make -C doc html + +.PHONY: check-copyright +check-copyright: + ./scripts/copyright.py --check . + +.PHONY: update-copyright +update-copyright: + ./scripts/copyright.py --update . diff --git a/README b/README new file mode 120000 index 0000000..712aeba --- /dev/null +++ b/README @@ -0,0 +1 @@ +doc/source/usage.rst \ No newline at end of file diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..86d5906 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,153 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/typesafety.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/typesafety.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/typesafety" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/typesafety" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/doc/source/_static/balabit_200.gif b/doc/source/_static/balabit_200.gif new file mode 100644 index 0000000..79bbb6d Binary files /dev/null and b/doc/source/_static/balabit_200.gif differ diff --git a/doc/source/_templates/.placeholder b/doc/source/_templates/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/doc/source/apiref.rst b/doc/source/apiref.rst new file mode 100644 index 0000000..3297ba2 --- /dev/null +++ b/doc/source/apiref.rst @@ -0,0 +1,7 @@ +======================== +typesafety API reference +======================== + +.. automodule:: typesafety + :members: + :show-inheritance: diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..5077a4c --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# typesafety documentation build configuration file, created by +# sphinx-quickstart on Thu Aug 29 11:15:49 2013. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os.path + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + +# This is here to ensure that the proper version of Python is used for generating +# the documentation +import typesafety + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'typesafety' +copyright = '2013, Balabit IT Security Ltd.' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '1.0' +# The full version, including alpha/beta/rc tags. +release = '1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +html_theme_options = { + 'sidebarbgcolor': '#FFF', + 'sidebarbtncolor': '#23414d', + 'sidebartextcolor': '#303030', + 'sidebarlinkcolor': '#598077', + 'relbarbgcolor': '#fff', + 'relbartextcolor': '#303030', + 'relbarlinkcolor': '#598077', +} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None +html_logo = '_static/balabit_200.gif' + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'typesafetydoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'typesafety.tex', 'typesafety Documentation', + 'Balabit IT Security Ltd.', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'typesafety', 'typesafety Documentation', + ['Balabit IT Security Ltd.'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'typesafety', 'typesafety Documentation', + 'Balabit IT Security Ltd.', 'typesafety', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..c554651 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,32 @@ +.. typesafety documentation master file, created by + sphinx-quickstart on Thu Aug 29 11:15:49 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to typesafety's documentation! +====================================== + +Articles +-------- + +.. toctree:: + :maxdepth: 2 + + usage + + +Reference +--------- + +.. toctree:: + :maxdepth: 2 + + apiref + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/source/usage.rst b/doc/source/usage.rst new file mode 100644 index 0000000..5376d21 --- /dev/null +++ b/doc/source/usage.rst @@ -0,0 +1,147 @@ +============================ +Usage of the typesafety tool +============================ + +Type safety checker module for python. The type safety checker uses +a debugger to check each function call and return and check the +value types according to the annotations specified. + +Turning on the type safety checker +================================== + +The safety checker mechanism is used through the :class:`typesafety.Typesafety` +singleton class. The :func:`typesafety.activate` function can turn +the checking on and the :func:`typesafety.deactivate` can turn it off. + +Module testmod: + +.. code-block:: python + + def my_function(x : int): + return x + 1 + +Main: + +.. code-block:: python + + import typesafety + import imp + import testmod + + testmod.my_function(1.0) # No error + + typesafety.activate() + testmod = imp.reload(testmod) + testmod.my_function(1.0) # Will throw a TypeError + +Specifying functions +==================== + +A function with argument or return value annotations will be used +to implement the type safety check mechanism. For further information +on how annotations work, see the Python documentation. + +Type annotations +---------------- + +The simplest type safety check is when a singular type is specified +for an argument: + +.. code-block:: python + + def my_function(x : int) -> float: + return float(x) + 1.0 + + my_function(1) # Will return 2.0 + my_function(1.0) # Will throw a TypeError + +In this case on each call the type safety checker will validate that +the argument is an `int` and the return value is a `float`. + +Callable annotations +-------------------- + +Some conditions cannot be checked by `isinstance`. If the parameter needs +to be a callable object (i.e. function, object with `__call__` implemented, +etc.) we can annotate the argument or return value with a callable: + +.. code-block:: python + + def decorator(func : callable) -> callable: + # ... + return res + + @decorator + def my_function(x): + pass + + decorator(1) # Will throw a TypeError + +Multiple annotations +-------------------- + +If a tuple is specified in the annotation, then at least +one of the specified conditions must apply to the argument. + +.. code-block:: python + + def multiple_argument_types(number : (int, float)) -> (int, float): + return number + 1 + + multiple_argument_types(1) # Will return 2 + multiple_argument_types(1.0) # Will return 2.0 + multiple_argument_types('string') # Will throw a TypeError + +Generating documentation using annotations with Sphinx autodoc +============================================================== + +To avoid having to write parameter documentation manually, the +``typesafety.sphinxautodoc`` Sphinx extension is provided. It will +automatically add the typesafety annotations to the signatures that +Sphinx autodoc puts into the documentation. + +Decorator functions +------------------- + +Custom decorator functions often work like the following: + +.. code-block:: python + + from functools import wraps + + def some_decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + # Do some additional stuff, and then... + return func(*args, **kwargs) + + return wrapper + + @some_decorator + def my_annotated_function(x: int): + pass + +This way the documentation for ``my_annotated_function`` will use the +signature of the decorated function, ie. it will be just ``*args, +**kwargs`` which is not very helpful. Sadly, there is no out-of-the-box +solution for this problem, however, if the decorator is extended with +setting the ``decorated_function`` attribute of the wrapper function it +returns, then ``typesafety.sphinxautodoc`` will use that attribute to +read the signature from: + +.. code-block:: python + + def some_decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + # Do some additional stuff, and then... + return func(*args, **kwargs) + + + wrapper.decorated_function = func + + return wrapper + +Using the above version of ``@some_decorator`` will enable +``typesafety.sphinxautodoc`` to generate the proper signature +documentation for ``my_annotated_function()``, ie. ``(x: int)``. diff --git a/scripts/copyright.py b/scripts/copyright.py new file mode 100755 index 0000000..610a9b1 --- /dev/null +++ b/scripts/copyright.py @@ -0,0 +1,209 @@ +#!/usr/bin/python3 +# +# Copyright (c) 2013-2015 BalaBit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import collections +import argparse +import os.path +import datetime +import re +import stat + + +def load_pattern_file(fn): + res = [] + with open(fn) as f: + for line in f: + line = line.split('#', 1)[0].strip() + + if not line: + continue + + res.append(re.compile(line)) + + return res + + +def match_patterns(text, patterns): + return any(pattern.search(text) is not None for pattern in patterns) + + +THIS_YEAR = datetime.datetime.now().year +BASE_PATH = os.path.dirname(__file__) +EXCLUDE_DIRS = load_pattern_file(os.path.join(BASE_PATH, 'copyright_dirs.exclude')) +EXCLUDE_FILES = load_pattern_file(os.path.join(BASE_PATH, 'copyright_files.exclude')) +with open(os.path.join(BASE_PATH, 'copyright.txt')) as f: + COPYRIGHT_TEXT = f.read() +FILTER_EXTENSION = {'.py', '.sh'} +BEGIN_PATTERN = re.compile(r"^Copyright \(c\) 2013-(20[0-9]{2}) BalaBit$") + +UPDATE_COPYRIGHT_HEADER = ''' +Copyright (c) 2013-{year} BalaBit +{copyright}\ +'''.format(year=THIS_YEAR, copyright=COPYRIGHT_TEXT) + + +class CommentError(Exception): + def __init__(self, filename, message): + super().__init__(message) + + self.filename = filename + self.message = message + + +def locate_files(base_path): + for name in os.listdir(base_path): + fullpath = os.path.join(base_path, name) + + if os.path.isfile(fullpath): + ext = os.path.splitext(name)[1] + if ext not in FILTER_EXTENSION: + continue + + if match_patterns(fullpath, EXCLUDE_FILES): + continue + + if os.lstat(fullpath).st_size == 0: + continue + + yield fullpath + + elif os.path.isdir(fullpath): + if match_patterns(fullpath, EXCLUDE_DIRS): + continue + + for res in locate_files(fullpath): + yield res + + +HeaderComment = collections.namedtuple('HeaderComment', ['begin_line', 'end_line', 'body', 'has_hashbang']) + + +def get_header_comment(filename): + begin_line = None + end_line = 0 + has_hashbang = False + comment = [] + + with open(filename) as f: + for index, line in enumerate(f): + line = line.strip() + + if not line or line[0] != '#': + end_line = index + break + + if line[:2].startswith('#!'): + has_hashbang = True + continue + + if begin_line is None: + begin_line = index + + if line == '#': + comment.append('') + continue + + if line[:2] != '# ': + raise CommentError(filename, "Invalid comment line format") + + comment.append(line[2:]) + + if begin_line is None: + begin_line = end_line + + return HeaderComment( + begin_line=begin_line, + end_line=end_line, + body=comment, + has_hashbang=has_hashbang + ) + + +def check_file_header(filename): + comment = get_header_comment(filename) + + if not comment.body: + raise CommentError(filename, 'Missing copyright header') + + if comment.body[0] != '' or comment.body[-1] != '': + raise CommentError(filename, 'Copyright header format invalid') + + match = BEGIN_PATTERN.match(comment.body[1]) + if match is None: + raise CommentError(filename, 'Copyright header format invalid') + + if int(match.group(1)) != THIS_YEAR: + raise CommentError(filename, 'Copyright year in headers invalid') + + if '\n'.join(comment.body[2:]) != COPYRIGHT_TEXT: + raise CommentError(filename, 'Copyright text not the expected one') + + +def update_file_header(filename): + try: + check_file_header(filename) + return + + except CommentError: + pass + + with open(filename) as f: + data = f.read().split('\n') + + header = get_header_comment(filename) + new_header_block = ['# {}'.format(line).rstrip() for line in UPDATE_COPYRIGHT_HEADER.split('\n')] + if not header.body: + # New comment line, add a newline after the block + new_header_block.append('') + + data[header.begin_line:header.end_line] = new_header_block + + with open(filename, 'w') as f: + f.write('\n'.join(data)) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Copyright header manager script') + group = parser.add_mutually_exclusive_group() + group.add_argument('--update', action='store_true', + help='Update existing headers and add missing ones.') + group.add_argument('--check', action='store_true', + help='Check existence and correctness of the header copyright information.') + parser.add_argument('path', help='Path(s) to check the files in.') + + options = parser.parse_args() + + rc = 0 + if options.check: + for filename in locate_files(options.path): + try: + check_file_header(filename) + + except CommentError as exc: + print('{}: {}'.format(exc.filename, exc.message)) + rc = 1 + + elif options.update: + for filename in locate_files(options.path): + update_file_header(filename) + + else: + raise NotImplementedError + rc = 1 + + exit(rc) diff --git a/scripts/copyright.txt b/scripts/copyright.txt new file mode 100644 index 0000000..d5daaf6 --- /dev/null +++ b/scripts/copyright.txt @@ -0,0 +1,13 @@ +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/scripts/copyright_dirs.exclude b/scripts/copyright_dirs.exclude new file mode 100644 index 0000000..f89b7eb --- /dev/null +++ b/scripts/copyright_dirs.exclude @@ -0,0 +1,9 @@ +/\.git +/\.idea +/\.tox +/__pycache__ +/*egg-info +/assets +/build +/debian +/doc diff --git a/scripts/copyright_files.exclude b/scripts/copyright_files.exclude new file mode 100644 index 0000000..f8d162b --- /dev/null +++ b/scripts/copyright_files.exclude @@ -0,0 +1 @@ +xmlrunner.py diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..30e7232 --- /dev/null +++ b/setup.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2013-2015 BalaBit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + + +import sys +if sys.hexversion < 0x03020000: + raise RuntimeError("Required python version: 3.2 or newer (current: %s)" % sys.version) + +try: + from setuptools import setup + +except ImportError: + from distutils.core import setup + +setup( + name="typesafety", + description="Type safety checker for Python3", + long_description=\ + """ + Typesafety is a tool for writing type-checked code in Python. In languages + like C++, Java, etc., this is a language-level feature, but Python has no + such feature. With the advent of annotations it is however possible to write + code with type notations. Typesafety is a means to enforce that those notations + are valid. + """, + license="LGPLv2+", + version="1.0", + author="Viktor Hercinger", + author_email="viktor.hercinger@balabit.com", + maintainer="Viktor Hercinger", + maintainer_email="viktor.hercinger@balabit.com", + keywords='nose type typesafe static', + url="https://github.com/balabit/typesafety", + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Topic :: Software Development', + 'Topic :: Software Development :: Testing', + 'Topic :: Documentation :: Sphinx', + ], + zip_safe=True, + use_2to3=False, + packages=['typesafety'], + entry_points={ + 'nose.plugins.0.10': [ + 'typesafety = typesafety.noseplugin:TypesafetyPlugin' + ] + }, + requires=[ + 'nose', + 'sphinx', + ] +) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..d9ca567 --- /dev/null +++ b/tox.ini @@ -0,0 +1,39 @@ +[tox] +envlist=py34,py33,py32,docs + +[testenv] +whitelist_externals= + make +deps= + nose + mock + pep8 + pylint + coverage +setenv= + PYTHON={envpython} +commands= + make pre-commit + +[testenv:py32] +# Note that some of the dependences are given with exact versioning +# because previous versions do not support Python 3.2 anymore. +deps= + nose + mock + pep8 + logilab-common==0.62.1 + astroid==1.2.1 + pylint==1.3.1 + coverage + +[testenv:docs] +deps= + nose + sphinx +basepython= + python3.4 +setenv= + PYTHON={envpython} +commands= + make documentation diff --git a/typesafety/__init__.py b/typesafety/__init__.py new file mode 100644 index 0000000..4d63a91 --- /dev/null +++ b/typesafety/__init__.py @@ -0,0 +1,104 @@ +# +# Copyright (c) 2013-2015 BalaBit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +''' +Type safety checker module for python. The type safety checker uses +a debugger to check each function call and return and check the +value types according to the annotations specified. + +Accepted annotations for type checked functions are class objects, +callable objects or tuples containing class and callable objects. If +any of the annotations does not conform to this rule the annotation +will be ignored. +''' + +from .validator import Validator +from .finder import ModuleFinder + + +class Typesafety(object): + ''' + Singleton class for managing the type safety checker. + ''' + + __instance = None + + @classmethod + def instance(cls): + ''' + Return the singleton instance. + ''' + + if cls.__instance is None: + cls.__instance = cls() + + return cls.__instance + + def __init__(self): + self.__module_finder = None + + @property + def active(self): + ''' + Returns whether the type safety checker is enabled. + ''' + + return self.__module_finder is not None + + def activate(self, *, filter_func=None): + ''' + Activate the type safety checker. After the call all functions + that need to be checked will be. + ''' + + if self.active: + raise RuntimeError("Type safety check already active") + + self.__module_finder = ModuleFinder(Validator.decorate) + if filter_func is not None: + self.__module_finder.set_filter(filter_func) + self.__module_finder.install() + + def deactivate(self): + ''' + Deactivate the type safety checker. After the call no functions + will be checked. + ''' + + if not self.active: + raise RuntimeError("Type safety check inactive") + + self.__module_finder.uninstall() + + +def activate(*, filter_func=None): + ''' + Shorthand function for activating the type checking. + ''' + + Typesafety.instance().activate(filter_func=filter_func) + + +def deactivate(): + ''' + Shorthand function for deactivating the type checking. + ''' + + Typesafety.instance().deactivate() + + +__all__ = ['Typesafety', 'activate', 'deactivate'] diff --git a/typesafety/autodecorator.py b/typesafety/autodecorator.py new file mode 100644 index 0000000..74b06a5 --- /dev/null +++ b/typesafety/autodecorator.py @@ -0,0 +1,116 @@ +# +# Copyright (c) 2013-2015 BalaBit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +''' +Scan some class or module object and decorate it's functions and methods with +the supplied decorator function. + +Usage: + +>>> class MyClass(object): +... def mymethod(self): +... return 0 +>>> def log(func): +... def __wrapper(*args, **kwargs): +... print('Called function %s' % func.__name__) +... return func(*args, **kwargs) +... return __wrapper +>>> decorate_module(MyClass, decorator=log) +>>> obj = MyClass() +>>> print(obj.mymethod()) +Called function mymethod +0 +''' + +import inspect + + +class ModuleDecorator(object): + ''' + Decorate a module automatically with the supplied decorator. + This is just a helper class for the :func:`decorate` module function. + + The `decorator` argument is the decorator to be applied to functions. + ''' + + def __init__(self, decorator): + self.__decorator = decorator + + def decorate(self, module): + self.__decorate(module, use_dict=module.__dict__) + + def __is_attribute_mutable(self, object_dict, attr): + if '__slots__' not in object_dict: + return True + + return attr in object_dict['__slots__'] + + def __decorate(self, module, *, use_dict): + for key, value in use_dict.items(): + if not self.__is_attribute_mutable(use_dict, key): + continue + + if inspect.isfunction(value): + self.__decorate_function(module, key, value) + + elif inspect.isclass(value): + self.__decorate(value, use_dict=value.__dict__) + + elif inspect.ismodule(value): + if self.__submodule_of(value, module): + self.__decorate(value, use_dict=value.__dict__) + + elif isinstance(value, property): + self.__decorate_property(module, key, value) + + elif isinstance(value, (staticmethod, classmethod)): + self.__decorate_special_method(module, key, value) + + def __decorate_function(self, module, key, value): + setattr(module, key, self.__decorator(value)) + + def __decorate_property(self, module, key, value): + fget = None + fset = None + fdel = None + + if value.fget is not None: + fget = self.__decorator(value.fget) + + if value.fset is not None: + fset = self.__decorator(value.fset) + + if value.fdel is not None: + fdel = self.__decorator(value.fdel) + + setattr(module, key, property(fget=fget, fset=fset, fdel=fdel)) + + def __decorate_special_method(self, module, key, value): + func = self.__decorator(value.__func__) + decorated = value.__class__(func) + + setattr(module, key, decorated) + + def __submodule_of(self, basemodule, submodule): + return submodule.__name__.startswith(basemodule.__name__ + '.') + + +def decorate_module(module, *, decorator): + ModuleDecorator(decorator).decorate(module) + + +__all__ = ['decorate_module'] diff --git a/typesafety/finder.py b/typesafety/finder.py new file mode 100644 index 0000000..1fb0ecc --- /dev/null +++ b/typesafety/finder.py @@ -0,0 +1,174 @@ +# +# Copyright (c) 2013-2015 BalaBit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +''' +On import decorate imported modules with the supplied decorator. +''' + +import imp +import importlib.abc +import sys +from . import autodecorator + + +class ModuleLoader(object): + ''' + Simple proxy object that will call the :func:`ModuleFinder.load_module` + function with the proper parameters. + ''' + + def __init__(self, finder, fullname, path): + self.__finder = finder + self.__fullname = fullname + if path is None: + self.__name = fullname + + else: + self.__name = fullname.rsplit('.', 1)[-1] + + self.__path = path + self.__info = imp.find_module(self.__name, path) + + @property + def fullname(self): + return self.__fullname + + @property + def name(self): + return self.__name + + @property + def info(self): + return self.__info + + def load_module(self, fullname): + ''' + Load the module. Required for the Python meta-loading mechanism. + ''' + + return self.__finder.load_module(self) + + +class ModuleFinder(object): + ''' + Module finder and loader. When installed, modules will be + automatically decorated with the supplied decorator. + + The `decorator` is the decorator function to apply. + + The `filter` argument is a filter function that should return + True if the given module should be decorated. This function takes + two arguments: + + * The full name of the module and + * the module object itself (after import). + ''' + + __decorator = None + __filter = None + __loaded_modules = None + + def __init__(self, decorator): + self.__decorator = decorator + self.__reset() + + @property + def installed(self): + ''' + True if the module finder/loader has been installed. + ''' + + return self in sys.meta_path + + def set_filter(self, filter_func): + ''' + Set the module filter function. + + The `filter_func` argument expects a callable that gets the full + module name and the loaded module object. + ''' + + self.__filter = filter_func + + def install(self): + ''' + Install the module finder. If already installed, this will do nothing. + After installation, each loaded module will be decorated. + ''' + + if not self.installed: + sys.meta_path.insert(0, self) + + def uninstall(self): + ''' + Uninstall the module finder. If not installed, this will do nothing. + After uninstallation, none of the newly loaded modules will be + decorated (that is, everything will be back to normal). + ''' + + if self.installed: + sys.meta_path.remove(self) + + # Reload all decorated items + import_list = [] + for name in self.__loaded_modules: + del sys.modules[name] + import_list.append(name) + + for name in import_list: + __import__(name) + + self.__reset() + + def find_module(self, fullname, path=None): + ''' + Find the module. Required for the Python meta-loading mechanism. + + This will do nothing, since we use the system to locate a module. + ''' + + if self.__filter is None or self.__filter(fullname): + return ModuleLoader(self, fullname, path) + + def load_module(self, loader): + ''' + Load the module. Required for the Python meta-loading mechanism. + ''' + + modfile, pathname, description = loader.info + module = imp.load_module( + loader.fullname, + modfile, + pathname, + description + ) + sys.modules[loader.fullname] = module + self.__loaded_modules.add(loader.fullname) + + autodecorator.decorate_module(module, decorator=self.__decorator) + + return module + + def __reset(self): + self.__loaded_modules = set() + + +importlib.abc.Loader.register(ModuleLoader) +importlib.abc.Finder.register(ModuleFinder) + + +__all__ = ['ModuleFinder'] diff --git a/typesafety/noseplugin.py b/typesafety/noseplugin.py new file mode 100644 index 0000000..5391959 --- /dev/null +++ b/typesafety/noseplugin.py @@ -0,0 +1,107 @@ +# +# Copyright (c) 2013-2015 BalaBit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import re +import os.path +import typesafety +import nose + + +# Monkey-patching the _exc_info_to_string is the only simple way to influence +# how the exception gets formatted. We can't really replace the object with a +# subclass (we use this in a nose plugin, which is one of the many classes +# modifying the TestResult class). +class ExceptionStringConverter: # pylint: disable=W0212 + TRACE_LINE_PATTERN = re.compile(r'^ File "([^"]*)"') + + @classmethod + def wrap_results_object(cls, result): + result._exc_info_to_string = cls(result._exc_info_to_string) + + def __init__(self, original_converter): + self.__original_converter = original_converter + + def __call__(self, err, test): + res = [] + skip_line = False + for line in self.__original_converter(err, test).split('\n'): + if skip_line: + skip_line = False + continue + + match = self.TRACE_LINE_PATTERN.match(line) + if match is not None and self.__should_skip(match.group(1)): + skip_line = True + continue + + res.append(line) + + return '\n'.join(res) + + def __should_skip(self, filename): + relative = os.path.relpath(filename, typesafety.__path__[0]) + return not relative.startswith('..') + + +class TypesafetyPlugin(nose.plugins.Plugin): + name = 'typesafety' + __enabled_for = () + keep_typesafety_trace = False + enabled = False + + def __init__(self, *args, activate=None, **kwargs): + super().__init__(*args, **kwargs) + + self.__activate = activate or typesafety.activate + + def options(self, parser, env): + parser.add_option( + '-T', '--enable-typesafety', action='append', metavar='MODULE', + help='Enable typesafety for the given modules' + ) + parser.add_option( + '--keep-typesafety-trace', action='store_true', + help='Do not hide typesafety traceback frames ' + + '(useful when debugging typesafety)' + ) + + def configure(self, options, config): + if options.enable_typesafety: + self.enabled = True + self.__enabled_for = tuple( + mod.split('.') for mod in options.enable_typesafety + ) + try: + self.__activate(filter_func=self.__check_need_activate) + + except RuntimeError: + # Nose plugin was already enabled in a different thread + return + + self.keep_typesafety_trace = options.keep_typesafety_trace + + # This is an interface function that cannot be removed (see the nose + # plugin documentation for the meaning of this function). + def prepareTestResult(self, result): # pylint: disable=C0103 + if not self.keep_typesafety_trace: + ExceptionStringConverter.wrap_results_object(result) + + def __check_need_activate(self, module_name): + module_name = module_name.split('.') + return any( + module_name[:len(name)] == name for name in self.__enabled_for + ) diff --git a/typesafety/sphinxautodoc.py b/typesafety/sphinxautodoc.py new file mode 100644 index 0000000..406440b --- /dev/null +++ b/typesafety/sphinxautodoc.py @@ -0,0 +1,186 @@ +# +# Copyright (c) 2013-2015 BalaBit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import collections +import inspect + + +# This is a sphinx interface implementation so the number of +# arguments is outside our control. +def add_annotations_to_signature( + app, + what, + name, + obj, + options, + signature, + return_annotation +): # pylint: disable=R0913 + if what not in {"function", "method", "class"}: + return None + + if what == "class": + obj = obj.__init__ + what = "method" + + if not inspect.isfunction(obj): + return None + + func = __extract_decorated_function(obj) + arg_spec = inspect.getfullargspec(func) + + if not arg_spec.annotations: + return None + + return __format_signature(what, func, arg_spec) + + +def __should_stop_resolution(obj, depth, *, maxdepth=100): + return depth >= maxdepth or \ + not hasattr(obj, "decorated_function") or \ + not inspect.isfunction(obj.decorated_function) + + +def __extract_decorated_function(obj): + depth = 0 + + while not __should_stop_resolution(obj, depth): + obj = obj.decorated_function + depth += 1 + + return obj + + +def __format_signature(type_str, function, arg_spec): + return_type = __format_return_type(arg_spec) + args = __format_args_and_kwargs(type_str, function, arg_spec) + + return (args, return_type) + + +def __format_return_type(arg_spec): + annotations = arg_spec.annotations + + if "return" not in annotations: + return "" + + return __format_type_annotation(annotations["return"]) + + +def __format_type_annotation(annotation): + if hasattr(annotation, "__name__"): + result = annotation.__name__ + module = getattr(annotation, '__module__', 'builtins') + if module != "builtins": + result = module + '.' + result + + return result + + if isinstance(annotation, str): + return repr(annotation) + + if isinstance(annotation, collections.Sequence): + annotation_sequence = ", ".join( + __format_type_annotation(t) for t in annotation + ) + return "(" + annotation_sequence + ")" + + return str(annotation) + + +def __format_args_and_kwargs(type_str, function, arg_spec): + args = __format_positional_args(type_str, function, arg_spec) + annotations = arg_spec.annotations + + if arg_spec.varargs is not None: + args.append("*%s" % __format_arg(arg_spec.varargs, annotations, {})) + + if arg_spec.kwonlyargs: + args.append("*") + args.extend( + __format_args( + arg_spec.kwonlyargs, + annotations, + arg_spec.kwonlydefaults + ) + ) + + if arg_spec.varkw is not None: + args.append("**%s" % __format_arg(arg_spec.varkw, annotations, {})) + + return "(%s)" % ", ".join(args) + + +def __format_positional_args(type_str, function, arg_spec): + arg_names, defaults = __remove_self_or_cls(type_str, function, arg_spec) + defaults_by_name = __get_defaults_as_dict(arg_names, defaults) + + return __format_args(arg_names, arg_spec.annotations, defaults_by_name) + + +def __remove_self_or_cls(type_str, function, arg_spec): + arg_names = arg_spec.args + defaults = arg_spec.defaults + + if type_str == "method" or inspect.ismethod(function): + if defaults is not None and len(defaults) == len(arg_names): + defaults = defaults[1:] + + arg_names = arg_names[1:] + + return arg_names, defaults + + +def __get_defaults_as_dict(arg_names, defaults): + defaults = defaults if defaults is not None else () + offset = len(arg_names) - len(defaults) + + return dict(zip(arg_names[offset:], defaults)) + + +def __format_args(arg_names, annotations, defaults_by_name): + args = [] + + for arg_name in arg_names: + args.append(__format_arg(arg_name, annotations, defaults_by_name)) + + return args + + +def __format_arg(arg_name, annotations, defaults_by_name): + name = __format_argument_name(arg_name, annotations) + default_value_str = __format_default_value(arg_name, defaults_by_name) + + return "%s%s" % (name, default_value_str) + + +def __format_argument_name(arg_name, annotations): + if arg_name not in annotations: + return arg_name + + return arg_name + ": " + __format_type_annotation(annotations[arg_name]) + + +def __format_default_value(arg_name, defaults_by_name): + if not defaults_by_name or arg_name not in defaults_by_name: + return "" + + return "=%s" % repr(defaults_by_name[arg_name]) + + +def setup(app): + app.connect("autodoc-process-signature", add_annotations_to_signature) diff --git a/typesafety/tests/__init__.py b/typesafety/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/typesafety/tests/mockmodule.py b/typesafety/tests/mockmodule.py new file mode 100644 index 0000000..2115d06 --- /dev/null +++ b/typesafety/tests/mockmodule.py @@ -0,0 +1,58 @@ +# +# Copyright (c) 2013-2015 BalaBit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +from . import mockmodule2 +from .version import is_above_version +import collections +import sys + + +if not is_above_version('3.2'): + class ClassWithSlots: + __slots__ = ['mutable'] + + @property + def immutable(self): + return 1 + + @property + def mutable(self): + return 2 + + +def function(): + pass + + +class ModuleClass(object): + def __init__(self): + super(ModuleClass, self).__init__() + + def method(self): + pass + + @property + def value(self): + pass + + @classmethod + def clsmethod(cls): + pass + + @staticmethod + def staticmethod(): + pass diff --git a/typesafety/tests/mockmodule2.py b/typesafety/tests/mockmodule2.py new file mode 100644 index 0000000..e2fea71 --- /dev/null +++ b/typesafety/tests/mockmodule2.py @@ -0,0 +1,19 @@ +# +# Copyright (c) 2013-2015 BalaBit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +# Empty module, only used to test if +# imports work from imported modules diff --git a/typesafety/tests/test_autodecorator.py b/typesafety/tests/test_autodecorator.py new file mode 100644 index 0000000..c2ffb3f --- /dev/null +++ b/typesafety/tests/test_autodecorator.py @@ -0,0 +1,78 @@ +# +# Copyright (c) 2013-2015 BalaBit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import sys +import unittest + +from typesafety.autodecorator import decorate_module +from .version import skip_above_version + + +def mock_decorator(func): + if not hasattr(func, '__name__'): + return func + + if func.__name__ == '__init__': + return func + + def __wrapper(*args, **kwargs): + return 1234 + + return __wrapper + + +class TestAutodecorate(unittest.TestCase): + def setUp(self): + from . import mockmodule + self._module = mockmodule + decorate_module(self._module, decorator=mock_decorator) + + def tearDown(self): + self.__unload_test_module('mockmodule') + self.__unload_test_module('mockmodule2') + + def test_module_function(self): + self.assertEquals(1234, self._module.function()) + + def test_object_method(self): + self.assertEquals(1234, self._module.ModuleClass().method()) + + def test_object_property(self): + self.assertEquals(1234, self._module.ModuleClass().value) + + def test_object_classmethod(self): + self.assertEquals(1234, self._module.ModuleClass.clsmethod()) + + def test_object_staticmethod(self): + self.assertEquals(1234, self._module.ModuleClass.staticmethod()) + + def __unload_test_module(self, name): + fullname = 'typesafety.tests.' + name + + if fullname in sys.modules: + del sys.modules[fullname] + + @skip_above_version( + "3.2", + "Defining slots this way causes a ValueError, so they become " + + "unreachable for the autodecorator. Every other fix like this " + + "in other software simply removes slot entries " + + "like ClassWithSlots.mutable." + ) + def test_immutable_class_attributes_are_not_decorated(self): + self.assertEquals(1, self._module.ClassWithSlots().immutable) + self.assertEquals(1234, self._module.ClassWithSlots().mutable) diff --git a/typesafety/tests/test_finder.py b/typesafety/tests/test_finder.py new file mode 100644 index 0000000..1b71e54 --- /dev/null +++ b/typesafety/tests/test_finder.py @@ -0,0 +1,68 @@ +# +# Copyright (c) 2013-2015 BalaBit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import sys +import unittest + +from typesafety.finder import ModuleFinder +from typesafety.autodecorator import decorate_module + + +def mock_decorator(func): + func.__decorated__ = True + return func + + +def isdecorated(func): + return getattr(func, '__decorated__', False) + + +def undecorate(func): + if hasattr(func, '__decorated__'): + del func.__decorated__ + + return func + + +class TestModuleFinder(unittest.TestCase): + def setUp(self): + self.finder = ModuleFinder(mock_decorator) + + def tearDown(self): + self.finder.uninstall() + + def test_module_found(self): + self.finder.install() + import typesafety.tests.mockmodule + self.assertTrue(self.finder.installed) + self.assertTrue( + isdecorated(typesafety.tests.mockmodule.function) + ) + self.assertTrue( + isdecorated(typesafety.tests.mockmodule.ModuleClass.method) + ) + + def test_module_found_but_filtered_out(self): + self.finder.set_filter(lambda name: False) + self.finder.install() + import typesafety.tests.mockmodule + self.assertFalse( + isdecorated(typesafety.tests.mockmodule.function) + ) + self.assertFalse( + isdecorated(typesafety.tests.mockmodule.ModuleClass.method) + ) diff --git a/typesafety/tests/test_plugin.py b/typesafety/tests/test_plugin.py new file mode 100644 index 0000000..26c6f7f --- /dev/null +++ b/typesafety/tests/test_plugin.py @@ -0,0 +1,98 @@ +# +# Copyright (c) 2013-2015 BalaBit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import nose +import unittest +import optparse +import shlex +import mock + +from ..noseplugin import TypesafetyPlugin + + +class TestPlugin(unittest.TestCase): + def __activate_call(self, *, filter_func=None): + self.filter = filter_func + + def setUp(self): + self.filter = None + self.plugin = TypesafetyPlugin(activate=self.__activate_call) + + def test_is_nose_plugin(self): + self.assertIsInstance(self.plugin, nose.plugins.Plugin) + + def __get_options(self, enable_for=()): + res = type( + 'Options', + (object,), + { + 'enable_typesafety': list(enable_for), + 'keep_typesafety_trace': False, + } + ) + + return res() + + def test_not_enabled_by_default(self): + self.assertFalse(self.plugin.enabled) + + def test_name(self): + self.assertEqual('typesafety', self.plugin.name) + + def test_options(self): + parser = optparse.OptionParser() + self.plugin.options(parser, {}) + + try: + opts, _ = parser.parse_args( + shlex.split( + 'nosetests3 --enable-typesafety example ' + + '--enable-typesafety example2' + ) + ) + + except SystemExit as exc: + self.fail('Option parser exited with code {}'.format(exc)) + + self.assertEqual( + ['example', 'example2'], + opts.enable_typesafety + ) + + def test_not_enabled_without_modules_given(self): + self.plugin.configure(self.__get_options(), None) + + self.assertFalse(self.plugin.enabled) + + def test_enabled_with_at_least_one_module_given(self): + self.plugin.configure( + self.__get_options(('example',)), + None + ) + + self.assertTrue(self.plugin.enabled) + + def test_activate_called_with_filter_func(self): + self.plugin.configure( + self.__get_options(('example', 'example2')), + None + ) + + self.assertTrue(self.filter('example')) + self.assertTrue(self.filter('example2')) + self.assertFalse(self.filter('example3')) + self.assertTrue(self.filter('example.submodule')) diff --git a/typesafety/tests/test_sphinxautodoc.py b/typesafety/tests/test_sphinxautodoc.py new file mode 100644 index 0000000..49aa4de --- /dev/null +++ b/typesafety/tests/test_sphinxautodoc.py @@ -0,0 +1,379 @@ +# +# Copyright (c) 2013-2015 BalaBit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import unittest +import collections + +from functools import wraps +from .version import skip_above_version, skip_below_or_at_version + +from typesafety.sphinxautodoc import add_annotations_to_signature + + +class TestAnnotatedDocsForMethodSignatures(unittest.TestCase): + def test_only_functions_classes_and_and_methods_are_considered(self): + self.__assert_signature_docs_override( + None, + "module", + "unittest", + unittest + ) + + def __assert_signature_docs_override( + self, + expected_override, + type_str, + fully_qualified_name_suffix, + obj + ): + signature_override = add_annotations_to_signature( + app=None, + what=type_str, + name="%s.%s" % (self.__module__, fully_qualified_name_suffix), + obj=obj, + options={}, + signature="()", + return_annotation=None + ) + self.assertEqual(expected_override, signature_override) + + def test_when_there_are_no_annots_then_sign_docs_are_not_overridden(self): + self.__assert_signature_docs_override( + None, + "function", + "function_without_annotations", + function_without_annotations + ) + + def test_when_args_have_annots_then_signature_docs_are_overridden(self): + self.__assert_signature_docs_override( + ("(an_int: int)", ""), + "method", + "ExampleClass.classmethod_with_builtin_type_annotations", + ExampleClass.classmethod_with_builtin_type_annotations + ) + + def test_when_retval_has_annot_then_signature_docs_are_overridden(self): + self.__assert_signature_docs_override( + ("()", "str"), + "function", + "function_with_return_annotation_only", + function_with_return_annotation_only + ) + + def test_class_signatures_are_overridden_according_to_their_init(self): + self.__assert_signature_docs_override( + ("(an_int: int)", ""), + "class", + "ExampleClass", + ExampleClass + ) + + def test_cls_sign_are_not_overridden_when_their_init_dont_cont_annot(self): + self.__assert_signature_docs_override( + None, + "class", + "SomeClass", + SomeClass + ) + self.__assert_signature_docs_override( + None, + "class", + "SomeOtherClass", + SomeOtherClass + ) + + def test_default_values_are_added_to_positional_args(self): + self.__assert_signature_docs_override( + ("(a_float: float, an_int: int=42, an_str: str='')", ""), + "method", + "ExampleClass.method_with_default_value", + ExampleClass.method_with_default_value + ) + + def test_first_arg_of_methods_is_ign_even_if_it_has_a_default_value(self): + self.__assert_signature_docs_override( + ("(an_int: int=42)", ""), + "method", + "ExampleClass.method_with_default_value_for_self", + ExampleClass.method_with_default_value_for_self + ) + + def test_default_value_can_be_complex(self): + self.__assert_signature_docs_override( + ("(a_tuple: tuple=(1, 2, 3))", ""), + "function", + "function_with_complex_default_value", + function_with_complex_default_value + ) + + def test_mixed_types_are_represented_as_tuples(self): + self.__assert_signature_docs_override( + ("(something: (None, int)=None)", "(int, None)"), + "function", + "function_with_mixed_types", + function_with_mixed_types + ) + + def test_string_annotations_dont_cause_infinite_recursion(self): + self.__assert_signature_docs_override( + ("(an_arg: 'some string')", ""), + "function", + "function_with_string_annotation", + function_with_string_annotation + ) + + def test_varargs_are_added_into_the_signature_when_present(self): + self.__assert_signature_docs_override( + ("(an_int: int, *varargs, **varkwargs)", ""), + "method", + "ExampleClass.method_with_varargs", + ExampleClass.method_with_varargs + ) + + def test_varargs_can_be_annotated(self): + self.__assert_signature_docs_override( + ("(an_int, *varargs: list, **varkwargs: dict)", ""), + "function", + "function_with_annotated_varargs", + function_with_annotated_varargs + ) + + def test_kwonly_args_are_appended_after_an_asterisk(self): + self.__assert_signature_docs_override( + ("(a_float: float, *, an_int: int)", ""), + "method", + "ExampleClass.method_with_kwonly_arg", + ExampleClass.method_with_kwonly_arg + ) + + def test_class_names_are_fully_qualified(self): + self.__assert_signature_docs_override( + ( + "(an_object: %s.SomeClass)" % self.__module__, + "%s.SomeClass" % self.__module__ + ), + "method", + "ExampleClass.method_with_class_names", + ExampleClass.method_with_class_names + ) + + def test_type_annotations_can_be_functions(self): + self.__assert_signature_docs_override( + ("(a_callable: callable)", "callable"), + "method", + "ExampleClass.method_with_function_annotations", + ExampleClass.method_with_function_annotations + ) + + def test_type_annotations_can_be_named_tuples(self): + self.__assert_signature_docs_override( + ("(a_tuple: %s.SomeNamedTuple)" % self.__module__, ""), + "function", + "function_with_named_tuple", + function_with_named_tuple + ) + + def test_decorated_methods_are_resolved_when_decor_func_attr_is_set(self): + self.__assert_signature_docs_override( + ("(an_int: int)", "str"), + "method", + "ExampleClass.method_with_decorators", + ExampleClass.method_with_decorators + ) + + @skip_above_version( + "3.3", + "For some reason, version(s) 3.3+ handle annotations of decorated " + "functions differently. In 3.4 this seems to be fixed." + ) + def test_decorated_methods_should_not_trigger_inifinite_loop(self): + self.__assert_signature_docs_override( + ("(*args, **kwargs)", ""), + "method", + "ExampleClass.method_with_recursive_decorator", + ExampleClass.method_with_recursive_decorator + ) + + @skip_below_or_at_version("3.3", "See the exact explanation above") + def test_decorated_methods_dont_trigger_inf_loop_but_return_none(self): + self.__assert_signature_docs_override( + None, + "method", + "ExampleClass.method_with_recursive_decorator", + ExampleClass.method_with_recursive_decorator + ) + + @skip_above_version( + "3.3", + "For some reason, version(s) 3.3+ handle annotations of decorated " + + "functions differently. In 3.4 this seems to be fixed." + ) + def test_decorated_methods_should_not_trigger_errors(self): + self.__assert_signature_docs_override( + ("(*args, **kwargs)", ""), + "method", + "ExampleClass.method_with_messed_up_decorator", + ExampleClass.method_with_messed_up_decorator + ) + + @skip_below_or_at_version("3.3", "See the exact explanation above") + def test_decorated_methods_should_not_trigger_errors_but_return_none(self): + self.__assert_signature_docs_override( + None, + "method", + "ExampleClass.method_with_messed_up_decorator", + ExampleClass.method_with_messed_up_decorator + ) + + def test_integration(self): + self.__assert_signature_docs_override( + ( + "(an_int: int, a_string: (None, str)=None, " + + "*, a_callable: (None, callable)=None, **kwargs: dict)", + "str" + ), + "method", + "ExampleClass.integration", + ExampleClass.integration + ) + + +class SomeClass: + def __init__(self): + pass + + +class SomeOtherClass: + def __init__(self): + pass + + +SomeNamedTuple = collections.namedtuple("SomeNamedTuple", "x") + + +def function_without_annotations(an_int): + pass + + +def function_with_return_annotation_only() -> str: + return "sample return value" + + +def function_with_mixed_types(something: (None, int)=None) -> (int, None): + return something + + +def function_with_named_tuple(a_tuple: SomeNamedTuple): + pass + + +def function_with_annotated_varargs(an_int, *varargs: list, **varkwargs: dict): + pass + + +def function_with_complex_default_value(a_tuple: tuple=(1, 2, 3)): + pass + + +def function_with_string_annotation(an_arg: "some string"): + pass + + +def some_decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + wrapper.decorated_function = func + + return wrapper + + +def recursive_decorator(func): + wrapper = some_decorator(func) + wrapper.decorated_function = wrapper + + return wrapper + + +def messed_up_decorator(func): + wrapper = some_decorator(func) + wrapper.decorated_function = "this is not a function" + + return wrapper + + +class ExampleClass: + @classmethod + def classmethod_with_builtin_type_annotations(cls, an_int: int): + pass + + def __init__(self, an_int: int): + pass + + def method_with_default_value( + self, + a_float: float, + an_int: int=42, + an_str: str="" + ): + pass + + def method_with_varargs(self, an_int: int, *varargs, **varkwargs): + pass + + def method_with_kwonly_arg(self, a_float: float, *, an_int: int): + pass + + def method_with_default_value_for_self(self=None, an_int: int=42): + pass + + def method_with_class_names(self, an_object: SomeClass) -> SomeClass: + return an_object + + def method_with_function_annotations( + self, + a_callable: callable + ) -> callable: + return a_callable + + @some_decorator + @some_decorator + def method_with_decorators(self, an_int: int) -> str: + return "" + + @recursive_decorator + def method_with_recursive_decorator(self, an_int: int): + return "" + + @messed_up_decorator + def method_with_messed_up_decorator(self, an_int: int): + return "" + + # This method is an example for some argument checks, so those + # arguments are needed even though they are not used. + # pylint: disable=W0612 + def integration( + self, + an_int: int, + a_string: (None, str)=None, + *, + a_callable: (None, callable)=None, + **kwargs: dict + ) -> str: + return "sample return value" diff --git a/typesafety/tests/test_validator.py b/typesafety/tests/test_validator.py new file mode 100644 index 0000000..003fdfc --- /dev/null +++ b/typesafety/tests/test_validator.py @@ -0,0 +1,167 @@ +# +# Copyright (c) 2013-2015 BalaBit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import unittest + +from typesafety.validator import Validator, TypesafetyError + + +def func_arg_validate( + intarg: int, + funcarg: callable, + tuplearg: (int, float) +) -> int: + return 1 + + +class TestValidator(unittest.TestCase): + def setUp(self): + self._validator = Validator(func_arg_validate) + + def test_function_has_no_annotation(self): + def func_no_annotation(x, y, z): + pass + + validator = Validator(func_no_annotation) + self.assertFalse(validator.need_validate_arguments) + self.assertFalse(validator.need_validate_return_value) + + def test_function_has_arg_annotation(self): + def func_arg_annotated(x: type): + pass + + validator = Validator(func_arg_annotated) + self.assertTrue(validator.need_validate_arguments) + self.assertFalse(validator.need_validate_return_value) + + def test_function_has_ret_annotation(self): + def func_ret_annotated(x) -> type: + pass + + validator = Validator(func_ret_annotated) + self.assertFalse(validator.need_validate_arguments) + self.assertTrue(validator.need_validate_return_value) + + def test_invalid_annotations_are_ignored(self): + def func_ignore_annotation(x: 1) -> (2,): + pass + + validator = Validator(func_ignore_annotation) + self.assertFalse(validator.need_validate_arguments) + self.assertFalse(validator.need_validate_return_value) + + def test_valid_arguments(self): + self._validator.validate_arguments( + dict(intarg=1, funcarg=func_arg_validate, tuplearg=1.0) + ) + self._validator.validate_arguments( + dict(intarg=1, funcarg=func_arg_validate, tuplearg=1) + ) + + def test_invalid_arguments(self): + self.assertRaises( + TypesafetyError, + self._validator.validate_arguments, + dict(intarg=1, funcarg=1, tuplearg=1) + ) + + def test_valid_return_value(self): + self._validator.validate_return_value(1) + + def test_invalid_return_value(self): + self.assertTrue(self._validator.need_validate_return_value) + self.assertRaises( + TypesafetyError, + self._validator.validate_return_value, + 1.0 + ) + + def test_validate_arguments_without_annotations(self): + def func_no_annotation(x): + pass + + Validator(func_no_annotation).validate_arguments(1) + + def test_validate_return_value_without_annotations(self): + def func_no_annotation(): + pass + + Validator(func_no_annotation).validate_return_value(1) + + def test_validate_with_ignored_annotations(self): + def func_ignore_annotation(x: 1): + pass + + Validator(func_ignore_annotation).validate_arguments(dict(x=1)) + + def test_call_validator(self): + self._validator(1, func_arg_validate, 1.0) + + def test_call_validator_with_bad_argument(self): + self.assertRaises(TypesafetyError, self._validator, 1, 2, 1.0) + + def test_call_validator_with_bad_return_value(self): + def func_bad_return() -> int: + pass + + self.assertRaises(TypesafetyError, Validator(func_bad_return)) + + def test_decorate_validated_function(self): + @Validator.decorate + def func_validate() -> int: + return 1 + + self.assertTrue(Validator.is_function_validated(func_validate)) + + def test_decorate_non_validated_function(self): + def func_dont_validate(): + pass + + self.assertEquals( + func_dont_validate, + Validator.decorate(func_dont_validate) + ) + + def test_undecorate_decorated_function(self): + def func_validate() -> int: + pass + + self.assertEquals( + func_validate, + Validator.undecorate(Validator.decorate(func_validate)) + ) + + def test_undecorate_not_decorated_function(self): + def func_dont_validate(): + pass + + self.assertEquals( + func_dont_validate, + Validator.undecorate(func_dont_validate) + ) + + def test_check_order_of_validation(self): + def func_has_an_error(intarg: int): + raise RuntimeError('error') + + self.assertRaises(TypesafetyError, Validator(func_has_an_error), 'a') + + def test_missing_required_argument(self): + def func_has_required_arg(intarg: int): + pass + + self.assertRaises(TypesafetyError, Validator(func_has_required_arg)) diff --git a/typesafety/tests/version.py b/typesafety/tests/version.py new file mode 100644 index 0000000..6256570 --- /dev/null +++ b/typesafety/tests/version.py @@ -0,0 +1,55 @@ +# +# Copyright (c) 2013-2015 BalaBit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import sys +import unittest + + +def process_version_string(version_string): + return tuple(int(v) for v in version_string.split('.')) + + +def version_skip(cond, version_string, extra_message=None): + msg = "Test is only run in Python " + version_string + if extra_message is not None: + msg += ": " + str(extra_message) + + return unittest.skipIf(cond, msg) + + +def is_above_version(version_string): + return sys.version_info[:2] > process_version_string(version_string) + + +def is_below_or_at_version(version_string): + return sys.version_info[:2] <= process_version_string(version_string) + + +def skip_above_version(version_string, extra_message=None): + return version_skip( + is_above_version(version_string), + version_string + '+', + extra_message=extra_message + ) + + +def skip_below_or_at_version(version_string, extra_message=None): + return version_skip( + is_below_or_at_version(version_string), + version_string + '-', + extra_message=extra_message + ) diff --git a/typesafety/validator.py b/typesafety/validator.py new file mode 100644 index 0000000..283d4a1 --- /dev/null +++ b/typesafety/validator.py @@ -0,0 +1,281 @@ +# +# Copyright (c) 2013-2015 BalaBit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import inspect + + +class TypesafetyError(Exception): + ''' + A type error detected by the Typesafety tool. Does not interfere + with the builtin TypeError so one does not accidentally write an + assertion in a unit test on a Typesafety error when intending to + assert on a TypeError raised by actual production code. + ''' + + +class Validator(object): + ''' + A Validator is a class that can check the function argument + and return value types as specified in the function annotations. + + The `function` argument must be a function, method or generator. + + The given function and it's annotations will be checked if they + conform to the following rules: + + * The annotation is a class (subclass of `type`), + * the annotation is a callable object (`callable` returns true on it) or + * is a tuple whose elements conform to the same rules. + + Any annotations not conforming to the above rules will be ignored + as they might belong to a different purpose. + ''' + + TYPE_ERROR_MESSAGE = "Argument {0} of function {1!r} is invalid " + \ + "(expected: {2}; got: {3})" + + @classmethod + def get_function_validator(cls, function): + ''' + Return the validator bound to `function` or None if the function + is not validated. + ''' + + return getattr(function, '__validator__', None) + + @classmethod + def is_function_validated(cls, function): + ''' + Return True if the function has a bound validator. + ''' + + return cls.get_function_validator(function) is not None + + @classmethod + def decorate(cls, function): + ''' + Decorate a function so the function call is checked whenever + a call is made. The calls that do not need any checks are skipped. + + The `function` argument is the function to be decorated. + + The return value will be either + + * the function itself, if there is nothing to validate, or + * a proxy function that will execute the validation. + ''' + + if cls.is_function_validated(function): + return function + + validator = cls(function) + + if not validator.need_validate_arguments and \ + not validator.need_validate_return_value: + return function + + def __wrapper(*args, **kwargs): + return validator(*args, **kwargs) + + __wrapper.__name__ = function.__name__ + __wrapper.__doc__ = function.__doc__ + __wrapper.__validator__ = validator + + return __wrapper + + @classmethod + def undecorate(cls, function): + ''' + Remove validator decoration from a function. + + The `function` argument is the function to be cleaned from + the validator decorator. + ''' + + if cls.is_function_validated(function): + return cls.get_function_validator(function).function + + return function + + def __init__(self, function): + self.__function = function + self.__spec = inspect.getfullargspec(function) + self.__argument_annotation = {} + self.__return_annotation = None + self.__defaults = {} + + self.__process_type_annotations() + self.__process_return_value_annotation() + self.__process_default_values() + + @property + def need_validate_arguments(self): + ''' + True if any of the function arguments need to be checked. + ''' + + return bool(self.__argument_annotation) + + @property + def need_validate_return_value(self): + ''' + True if the return value of the function needs to be be checked. + ''' + + return self.__return_annotation is not None + + @property + def function(self): + ''' + The raw function value. + ''' + + return self.__function + + def validate_arguments(self, locals_dict): + ''' + Validate the arguments passed to a function. If an error occurred, + the function will throw a TypesafetyError. + + The `locals_dict` argument should be the local value dictionary of + the function. An example call would be like: + ''' + + for key, value, validator in self.__map_arguments(locals_dict): + if not self.__is_valid(value, validator): + key_name = repr(key) + func_name = self.__function.__name__ + message = self.TYPE_ERROR_MESSAGE.format( + key_name, + func_name, + self.__argument_annotation.get(key).__name__, + value.__class__.__name__) + raise TypesafetyError(message) + + def validate_return_value(self, retval): + ''' + Validate the return value of a function call. If an error occurred, + the function will throw a TypesafetyError. + + The `retval` should contain the return value of the function call. + ''' + + if self.__return_annotation is None: + return + + if not self.__is_valid(retval, self.__return_annotation): + func_name = self.__function.__name__ + msg = "Return value of function {} is invalid".format(func_name) + raise TypesafetyError(msg) + + def __call__(self, *args, **kwargs): + ''' + Proxy function to the function call including the validations. + ''' + + locals_dict = self.__collect_argument_dictionary(args, kwargs) + self.validate_arguments(locals_dict) + + # The function property is callable, but pylint sees it as a + # simple property object. + return_value = self.function(*args, **kwargs) # pylint: disable=E1102 + + self.validate_return_value(return_value) + return return_value + + def __process_type_annotations(self): + for name, value in self.__spec.annotations.items(): + if name == 'return' or \ + not self.__is_valid_typecheck_annotation(value): + continue + + self.__argument_annotation[name] = value + + def __process_return_value_annotation(self): + if 'return' in self.__spec.annotations: + return_annotation = self.__spec.annotations['return'] + if self.__is_valid_typecheck_annotation(return_annotation): + self.__return_annotation = return_annotation + + def __process_default_values(self): + if self.__spec.defaults is not None: + args_with_defaults = self.__spec.args[-len(self.__spec.defaults):] + for index, name in enumerate(args_with_defaults): + self.__defaults[name] = self.__spec.defaults[index] + + if self.__spec.kwonlydefaults is not None: + for name in self.__spec.kwonlyargs: + if name in self.__spec.kwonlydefaults: + self.__defaults[name] = self.__spec.kwonlydefaults[name] + + def __collect_argument_dictionary(self, args, kwargs): + locals_dict = dict(self.__defaults) + + for index, value in enumerate(args): + if index < len(self.__spec.args): + locals_dict[self.__spec.args[index]] = value + + for name, value in kwargs.items(): + locals_dict[name] = value + + return locals_dict + + def __map_arguments(self, locals_dict): + for name in self.__spec.args + self.__spec.kwonlyargs: + if name not in self.__argument_annotation: + continue + + if name not in locals_dict: + msg = 'Missing required argument {!r}'.format(name) + raise TypesafetyError(msg) + + annotation = self.__argument_annotation[name] + yield name, locals_dict[name], annotation + + def __is_valid(self, value, validator): + if isinstance(validator, tuple): + return any( + self.__is_valid(value, subvalidator) + for subvalidator in validator + ) + + if isinstance(validator, type): + return isinstance(value, validator) + + if hasattr(validator, '__call__'): + return validator(value) + + # This line will probably never be reached + return True + + def __is_valid_typecheck_annotation(self, validator): + if isinstance(validator, tuple): + return all( + self.__is_valid_typecheck_annotation(subvalidator) + for subvalidator in validator + ) + + if isinstance(validator, type): + return True + + if callable(validator): + return True + + return False + + +__all__ = ['Validator']