Thu, 13 May 2021 18:10:26 +0200
Removed the included 'radon' library and have it as an external dependency installed during the plug-in installation (for eric > 21.5).
ChangeLog | file | annotate | diff | comparison | revisions | |
PKGLIST | file | annotate | diff | comparison | revisions | |
PluginMetricsRadon.epj | file | annotate | diff | comparison | revisions | |
PluginMetricsRadon.py | file | annotate | diff | comparison | revisions | |
PluginMetricsRadon.zip | file | annotate | diff | comparison | revisions | |
RadonMetrics/Documentation/source/Plugin_Metrics_Radon.PluginMetricsRadon.html | file | annotate | diff | comparison | revisions | |
RadonMetrics/radon/LICENSE | file | annotate | diff | comparison | revisions | |
RadonMetrics/radon/__init__.py | file | annotate | diff | comparison | revisions | |
RadonMetrics/radon/complexity.py | file | annotate | diff | comparison | revisions | |
RadonMetrics/radon/metrics.py | file | annotate | diff | comparison | revisions | |
RadonMetrics/radon/raw.py | file | annotate | diff | comparison | revisions | |
RadonMetrics/radon/visitors.py | file | annotate | diff | comparison | revisions |
--- a/ChangeLog Sun Apr 25 16:43:43 2021 +0200 +++ b/ChangeLog Thu May 13 18:10:26 2021 +0200 @@ -1,5 +1,9 @@ ChangeLog --------- +Version 4.0.0: +- removed the included 'radon' library and have it as an external + dependency installed during the plug-in installation (for eric > 21.5) + Version 3.1.0: - upgraded embedded Radon library to version 4.5.0 - implemented some code simplifications
--- a/PKGLIST Sun Apr 25 16:43:43 2021 +0200 +++ b/PKGLIST Thu May 13 18:10:26 2021 +0200 @@ -14,9 +14,3 @@ RadonMetrics/i18n/radon_en.qm RadonMetrics/i18n/radon_es.qm RadonMetrics/i18n/radon_ru.qm -RadonMetrics/radon/LICENSE -RadonMetrics/radon/__init__.py -RadonMetrics/radon/complexity.py -RadonMetrics/radon/metrics.py -RadonMetrics/radon/raw.py -RadonMetrics/radon/visitors.py
--- a/PluginMetricsRadon.epj Sun Apr 25 16:43:43 2021 +0200 +++ b/PluginMetricsRadon.epj Thu May 13 18:10:26 2021 +0200 @@ -61,7 +61,7 @@ "CopyrightMinFileSize": 0, "DocstringType": "eric", "EnabledCheckerCategories": "C, D, E, M, N, S, Y, W", - "ExcludeFiles": "*/Ui_*.py, */*_rc.py,*/radon/*", + "ExcludeFiles": "*/Ui_*.py, */*_rc.py", "ExcludeMessages": "C101,E265,E266,E305,E402,M201,M301,M302,M303,M304,M305,M306,M307,M308,M311,M312,M313,M314,M315,M321,M701,M702,M811,M834,N802,N803,N807,N808,N821,W293,W504,Y119,Y401,Y402", "FixCodes": "", "FixIssues": false, @@ -175,7 +175,6 @@ "PluginMetricsRadon.zip", "RadonMetrics/Documentation/LICENSE.GPL3", "RadonMetrics/Documentation/source", - "RadonMetrics/radon/LICENSE", "PluginMetricsRadon.epj" ], "OTHERTOOLSPARMS": { @@ -205,11 +204,6 @@ "RadonMetrics/MaintainabilityIndexDialog.py", "RadonMetrics/RawMetricsDialog.py", "RadonMetrics/__init__.py", - "RadonMetrics/radon/__init__.py", - "RadonMetrics/radon/complexity.py", - "RadonMetrics/radon/metrics.py", - "RadonMetrics/radon/raw.py", - "RadonMetrics/radon/visitors.py", "__init__.py" ], "SPELLEXCLUDES": "",
--- a/PluginMetricsRadon.py Sun Apr 25 16:43:43 2021 +0200 +++ b/PluginMetricsRadon.py Thu May 13 18:10:26 2021 +0200 @@ -27,7 +27,7 @@ author = "Detlev Offenbach <detlev@die-offenbachs.de>" autoactivate = True deactivateable = True -version = "3.1.0" +version = "4.0.0" className = "RadonMetricsPlugin" packageName = "RadonMetrics" shortDescription = "Code metrics plugin using radon package" @@ -984,5 +984,22 @@ if self.__projectRawMetricsDialog: self.__projectRawMetricsDialog.clear() + +def installDependencies(pipInstall): + """ + Function to install dependencies of this plug-in. + + @param pipInstall function to be called with a list of package names. + @type function + """ + try: + from radon import __version__ as radon_version + import Globals + if Globals.versionToTuple(radon_version) < (4, 5, 0): + # force an upgrade + pipInstall(["radon>=4.5.0"]) + except ImportError: + pipInstall(["radon>=4.5.0"]) + # # eflag: noqa = M801
--- a/RadonMetrics/Documentation/source/Plugin_Metrics_Radon.PluginMetricsRadon.html Sun Apr 25 16:43:43 2021 +0200 +++ b/RadonMetrics/Documentation/source/Plugin_Metrics_Radon.PluginMetricsRadon.html Thu May 13 18:10:26 2021 +0200 @@ -42,7 +42,11 @@ <h3>Functions</h3> <table> -<tr><td>None</td></tr> + +<tr> +<td><a href="#installDependencies">installDependencies</a></td> +<td>Function to install dependencies of this plug-in.</td> +</tr> </table> <hr /> <hr /> @@ -723,4 +727,21 @@ </dl> <div align="right"><a href="#top">Up</a></div> <hr /> +<hr /> +<a NAME="installDependencies" ID="installDependencies"></a> +<h2>installDependencies</h2> +<b>installDependencies</b>(<i>pipInstall</i>) + +<p> + Function to install dependencies of this plug-in. +</p> +<dl> + +<dt><i>pipInstall</i> (function)</dt> +<dd> +function to be called with a list of package names. +</dd> +</dl> +<div align="right"><a href="#top">Up</a></div> +<hr /> </body></html> \ No newline at end of file
--- a/RadonMetrics/radon/LICENSE Sun Apr 25 16:43:43 2021 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,22 +0,0 @@ -Copyright (c) 2012-2017 Michele Lacchia - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE.
--- a/RadonMetrics/radon/__init__.py Sun Apr 25 16:43:43 2021 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -'''This module contains the main() function, which is the entry point for the -command line interface.''' - -__version__ = '4.5.0' - - -def main(): - '''The entry point for Setuptools.''' - import sys - from radon.cli import program, log_error - - if not sys.argv[1:]: - sys.argv.append('-h') - try: - program() - except Exception as e: - log_error(e) - - -if __name__ == '__main__': - main()
--- a/RadonMetrics/radon/complexity.py Sun Apr 25 16:43:43 2021 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,108 +0,0 @@ -'''This module contains all high-level helpers function that allow to work with -Cyclomatic Complexity -''' - -import math - -from radon.visitors import GET_COMPLEXITY, ComplexityVisitor, code2ast - -# sorted_block ordering functions -SCORE = lambda block: -GET_COMPLEXITY(block) -LINES = lambda block: block.lineno -ALPHA = lambda block: block.name - - -def cc_rank(cc): - r'''Rank the complexity score from A to F, where A stands for the simplest - and best score and F the most complex and worst one: - - ============= ===================================================== - 1 - 5 A (low risk - simple block) - 6 - 10 B (low risk - well structured and stable block) - 11 - 20 C (moderate risk - slightly complex block) - 21 - 30 D (more than moderate risk - more complex block) - 31 - 40 E (high risk - complex block, alarming) - 41+ F (very high risk - error-prone, unstable block) - ============= ===================================================== - - Here *block* is used in place of function, method or class. - - The formula used to convert the score into an index is the following: - - .. math:: - - \text{rank} = \left \lceil \dfrac{\text{score}}{10} \right \rceil - - H(5 - \text{score}) - - where ``H(s)`` stands for the Heaviside Step Function. - The rank is then associated to a letter (0 = A, 5 = F). - ''' - if cc < 0: - raise ValueError('Complexity must be a non-negative value') - return chr( - min(int(math.ceil(cc / 10.0) or 1) - (1, 0)[5 - cc < 0], 5) + 65 - ) - - -def average_complexity(blocks): - '''Compute the average Cyclomatic complexity from the given blocks. - Blocks must be either :class:`~radon.visitors.Function` or - :class:`~radon.visitors.Class`. If the block list is empty, then 0 is - returned. - ''' - size = len(blocks) - if size == 0: - return 0 - return sum((GET_COMPLEXITY(block) for block in blocks), 0.0) / len(blocks) - - -def sorted_results(blocks, order=SCORE): - '''Given a ComplexityVisitor instance, returns a list of sorted blocks - with respect to complexity. A block is a either - :class:`~radon.visitors.Function` object or a - :class:`~radon.visitors.Class` object. - The blocks are sorted in descending order from the block with the highest - complexity. - - The optional `order` parameter indicates how to sort the blocks. It can be: - - * `LINES`: sort by line numbering; - * `ALPHA`: sort by name (from A to Z); - * `SCORE`: sorty by score (descending). - - Default is `SCORE`. - ''' - return sorted(blocks, key=order) - - -def add_inner_blocks(blocks): - '''Process a list of blocks by adding all closures and inner classes as - top-level blocks. - ''' - new_blocks = [] - all_blocks = blocks[:] - while all_blocks: - block = all_blocks.pop() - new_blocks.append(block) - for inner_block in ('closures', 'inner_classes'): - for i_block in getattr(block, inner_block, ()): - named = i_block._replace(name=block.name + '.' + i_block.name) - all_blocks.append(named) - for meth in getattr(named, 'methods', ()): - m_named = meth._replace(classname=named.name) - all_blocks.append(m_named) - return new_blocks - - -def cc_visit(code, **kwargs): - '''Visit the given code with :class:`~radon.visitors.ComplexityVisitor`. - All the keyword arguments are directly passed to the visitor. - ''' - return cc_visit_ast(code2ast(code), **kwargs) - - -def cc_visit_ast(ast_node, **kwargs): - '''Visit the AST node with :class:`~radon.visitors.ComplexityVisitor`. All - the keyword arguments are directly passed to the visitor. - ''' - return ComplexityVisitor.from_ast(ast_node, **kwargs).blocks
--- a/RadonMetrics/radon/metrics.py Sun Apr 25 16:43:43 2021 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,157 +0,0 @@ -'''Module holding functions related to miscellaneous metrics, such as Halstead -metrics or the Maintainability Index. -''' - -import ast -import collections -import math - -from radon.raw import analyze -from radon.visitors import ComplexityVisitor, HalsteadVisitor - -# Halstead metrics -HalsteadReport = collections.namedtuple( - 'HalsteadReport', - 'h1 h2 N1 N2 vocabulary length ' - 'calculated_length volume ' - 'difficulty effort time bugs', -) - -# `total` is a HalsteadReport for the entire scanned file, while `functions` is -# a list of `HalsteadReport`s for each function in the file. -Halstead = collections.namedtuple("Halstead", "total functions") - - -def h_visit(code): - '''Compile the code into an AST tree and then pass it to - :func:`~radon.metrics.h_visit_ast`. - ''' - return h_visit_ast(ast.parse(code)) - - -def h_visit_ast(ast_node): - ''' - Visit the AST node using the :class:`~radon.visitors.HalsteadVisitor` - visitor. The results are `HalsteadReport` namedtuples with the following - fields: - - * h1: the number of distinct operators - * h2: the number of distinct operands - * N1: the total number of operators - * N2: the total number of operands - * h: the vocabulary, i.e. h1 + h2 - * N: the length, i.e. N1 + N2 - * calculated_length: h1 * log2(h1) + h2 * log2(h2) - * volume: V = N * log2(h) - * difficulty: D = h1 / 2 * N2 / h2 - * effort: E = D * V - * time: T = E / 18 seconds - * bugs: B = V / 3000 - an estimate of the errors in the implementation - - The actual return of this function is a namedtuple with the following - fields: - - * total: a `HalsteadReport` namedtuple for the entire scanned file - * functions: a list of `HalsteadReport`s for each toplevel function - - Nested functions are not tracked. - ''' - visitor = HalsteadVisitor.from_ast(ast_node) - total = halstead_visitor_report(visitor) - functions = [ - (v.context, halstead_visitor_report(v)) - for v in visitor.function_visitors - ] - - return Halstead(total, functions) - - -def halstead_visitor_report(visitor): - """Return a HalsteadReport from a HalsteadVisitor instance.""" - h1, h2 = visitor.distinct_operators, visitor.distinct_operands - N1, N2 = visitor.operators, visitor.operands - h = h1 + h2 - N = N1 + N2 - if h1 and h2: - length = h1 * math.log(h1, 2) + h2 * math.log(h2, 2) - else: - length = 0 - volume = N * math.log(h, 2) if h != 0 else 0 - difficulty = (h1 * N2) / float(2 * h2) if h2 != 0 else 0 - effort = difficulty * volume - return HalsteadReport( - h1, - h2, - N1, - N2, - h, - N, - length, - volume, - difficulty, - effort, - effort / 18.0, - volume / 3000.0, - ) - - -def mi_compute(halstead_volume, complexity, sloc, comments): - '''Compute the Maintainability Index (MI) given the Halstead Volume, the - Cyclomatic Complexity, the SLOC number and the number of comment lines. - Usually it is not used directly but instead :func:`~radon.metrics.mi_visit` - is preferred. - ''' - if any(metric <= 0 for metric in (halstead_volume, sloc)): - return 100.0 - sloc_scale = math.log(sloc) - volume_scale = math.log(halstead_volume) - comments_scale = math.sqrt(2.46 * math.radians(comments)) - # Non-normalized MI - nn_mi = ( - 171 - - 5.2 * volume_scale - - 0.23 * complexity - - 16.2 * sloc_scale - + 50 * math.sin(comments_scale) - ) - return min(max(0.0, nn_mi * 100 / 171.0), 100.0) - - -def mi_parameters(code, count_multi=True): - '''Given a source code snippet, compute the necessary parameters to - compute the Maintainability Index metric. These include: - - * the Halstead Volume - * the Cyclomatic Complexity - * the number of LLOC (Logical Lines of Code) - * the percent of lines of comment - - :param multi: If True, then count multiline strings as comment lines as - well. This is not always safe because Python multiline strings are not - always docstrings. - ''' - ast_node = ast.parse(code) - raw = analyze(code) - comments_lines = raw.comments + (raw.multi if count_multi else 0) - comments = comments_lines / float(raw.sloc) * 100 if raw.sloc != 0 else 0 - return ( - h_visit_ast(ast_node).total.volume, - ComplexityVisitor.from_ast(ast_node).total_complexity, - raw.lloc, - comments, - ) - - -def mi_visit(code, multi): - '''Visit the code and compute the Maintainability Index (MI) from it.''' - return mi_compute(*mi_parameters(code, multi)) - - -def mi_rank(score): - r'''Rank the score with a letter: - - * A if :math:`\text{score} > 19`; - * B if :math:`9 < \text{score} \le 19`; - * C if :math:`\text{score} \le 9`. - ''' - return chr(65 + (9 - score >= 0) + (19 - score >= 0))
--- a/RadonMetrics/radon/raw.py Sun Apr 25 16:43:43 2021 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,245 +0,0 @@ -'''This module contains functions related to raw metrics. - -The main function is :func:`~radon.raw.analyze`, and should be the only one -that is used. -''' - -import collections -import operator -import tokenize - -try: - import StringIO as io -except ImportError: # pragma: no cover - import io - - -__all__ = [ - 'OP', - 'COMMENT', - 'TOKEN_NUMBER', - 'NL', - 'NEWLINE', - 'EM', - 'Module', - '_generate', - '_fewer_tokens', - '_find', - '_logical', - 'analyze', -] - -COMMENT = tokenize.COMMENT -OP = tokenize.OP -NL = tokenize.NL -NEWLINE = tokenize.NEWLINE -EM = tokenize.ENDMARKER - -# Helper for map() -TOKEN_NUMBER = operator.itemgetter(0) - -# A module object. It contains the following data: -# loc = Lines of Code (total lines) -# lloc = Logical Lines of Code -# comments = Comments lines -# multi = Multi-line strings (assumed to be docstrings) -# blank = Blank lines (or whitespace-only lines) -# single_comments = Single-line comments or docstrings -Module = collections.namedtuple( - 'Module', - ['loc', 'lloc', 'sloc', 'comments', 'multi', 'blank', 'single_comments'], -) - - -def _generate(code): - '''Pass the code into `tokenize.generate_tokens` and convert the result - into a list. - ''' - # tokenize.generate_tokens is an undocumented function accepting text - return list(tokenize.generate_tokens(io.StringIO(code).readline)) - - -def _fewer_tokens(tokens, remove): - '''Process the output of `tokenize.generate_tokens` removing - the tokens specified in `remove`. - ''' - for values in tokens: - if values[0] in remove: - continue - yield values - - -def _find(tokens, token, value): - '''Return the position of the last token with the same (token, value) - pair supplied. The position is the one of the rightmost term. - ''' - for index, token_values in enumerate(reversed(tokens)): - if (token, value) == token_values[:2]: - return len(tokens) - index - 1 - raise ValueError('(token, value) pair not found') - - -def _split_tokens(tokens, token, value): - '''Split a list of tokens on the specified token pair (token, value), - where *token* is the token type (i.e. its code) and *value* its actual - value in the code. - ''' - res = [[]] - for token_values in tokens: - if (token, value) == token_values[:2]: - res.append([]) - continue - res[-1].append(token_values) - return res - - -def _get_all_tokens(line, lines): - '''Starting from *line*, generate the necessary tokens which represent the - shortest tokenization possible. This is done by catching - :exc:`tokenize.TokenError` when a multi-line string or statement is - encountered. - :returns: tokens, lines - ''' - buffer = line - used_lines = [line] - while True: - try: - tokens = _generate(buffer) - except tokenize.TokenError: - # A multi-line string or statement has been encountered: - # start adding lines and stop when tokenize stops complaining - pass - else: - if not any(t[0] == tokenize.ERRORTOKEN for t in tokens): - return tokens, used_lines - - # Add another line - next_line = next(lines) - buffer = buffer + '\n' + next_line - used_lines.append(next_line) - - -def _logical(tokens): - '''Find how many logical lines are there in the current line. - - Normally 1 line of code is equivalent to 1 logical line of code, - but there are cases when this is not true. For example:: - - if cond: return 0 - - this line actually corresponds to 2 logical lines, since it can be - translated into:: - - if cond: - return 0 - - Examples:: - - if cond: -> 1 - - if cond: return 0 -> 2 - - try: 1/0 -> 2 - - try: -> 1 - - if cond: # Only a comment -> 1 - - if cond: return 0 # Only a comment -> 2 - ''' - - def aux(sub_tokens): - '''The actual function which does the job.''' - # Get the tokens and, in the meantime, remove comments - processed = list(_fewer_tokens(sub_tokens, [COMMENT, NL, NEWLINE])) - try: - # Verify whether a colon is present among the tokens and that - # it is the last token. - token_pos = _find(processed, OP, ':') - # We subtract 2 from the total because the last token is always - # ENDMARKER. There are two cases: if the colon is at the end, it - # means that there is only one logical line; if it isn't then there - # are two. - return 2 - (token_pos == len(processed) - 2) - except ValueError: - # The colon is not present - # If the line is only composed by comments, newlines and endmarker - # then it does not count as a logical line. - # Otherwise it count as 1. - if not list(_fewer_tokens(processed, [NL, NEWLINE, EM])): - return 0 - return 1 - - return sum(aux(sub) for sub in _split_tokens(tokens, OP, ';')) - - -def is_single_token(token_number, tokens): - '''Is this a single token matching token_number followed by ENDMARKER, NL - or NEWLINE tokens. - ''' - return TOKEN_NUMBER(tokens[0]) == token_number and all( - TOKEN_NUMBER(t) in (EM, NL, NEWLINE) for t in tokens[1:] - ) - - -def analyze(source): - '''Analyze the source code and return a namedtuple with the following - fields: - - * **loc**: The number of lines of code (total) - * **lloc**: The number of logical lines of code - * **sloc**: The number of source lines of code (not necessarily - corresponding to the LLOC) - * **comments**: The number of Python comment lines - * **multi**: The number of lines which represent multi-line strings - * **single_comments**: The number of lines which are just comments with - no code - * **blank**: The number of blank lines (or whitespace-only ones) - - The equation :math:`sloc + blanks + multi + single_comments = loc` should - always hold. Multiline strings are not counted as comments, since, to the - Python interpreter, they are not comments but strings. - ''' - lloc = comments = single_comments = multi = blank = sloc = 0 - lines = (l.strip() for l in source.splitlines()) - lineno = 1 - for line in lines: - try: - # Get a syntactically complete set of tokens that spans a set of - # lines - tokens, parsed_lines = _get_all_tokens(line, lines) - except StopIteration: - raise SyntaxError('SyntaxError at line: {0}'.format(lineno)) - - lineno += len(parsed_lines) - - comments += sum( - 1 for t in tokens if TOKEN_NUMBER(t) == tokenize.COMMENT - ) - - # Identify single line comments, conservatively - if is_single_token(tokenize.COMMENT, tokens): - single_comments += 1 - - # Identify docstrings, conservatively - elif is_single_token(tokenize.STRING, tokens): - _, _, (start_row, _), (end_row, _), _ = tokens[0] - if end_row == start_row: - # Consider single-line docstrings separately from other - # multiline docstrings - single_comments += 1 - else: - multi += sum(1 for l in parsed_lines if l) # Skip empty lines - blank += sum(1 for l in parsed_lines if not l) - else: # Everything else is either code or blank lines - for parsed_line in parsed_lines: - if parsed_line: - sloc += 1 - else: - blank += 1 - - # Process logical lines separately - lloc += _logical(tokens) - - loc = sloc + blank + multi + single_comments - return Module(loc, lloc, sloc, comments, multi, blank, single_comments)
--- a/RadonMetrics/radon/visitors.py Sun Apr 25 16:43:43 2021 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,453 +0,0 @@ -'''This module contains the ComplexityVisitor class which is where all the -analysis concerning Cyclomatic Complexity is done. There is also the class -HalsteadVisitor, that counts Halstead metrics.''' - -import ast -import collections -import operator - -# Helper functions to use in combination with map() -GET_COMPLEXITY = operator.attrgetter('complexity') -GET_REAL_COMPLEXITY = operator.attrgetter('real_complexity') -NAMES_GETTER = operator.attrgetter('name', 'asname') -GET_ENDLINE = operator.attrgetter('endline') - -BaseFunc = collections.namedtuple( - 'Function', - [ - 'name', - 'lineno', - 'col_offset', - 'endline', - 'is_method', - 'classname', - 'closures', - 'complexity', - ], -) -BaseClass = collections.namedtuple( - 'Class', - [ - 'name', - 'lineno', - 'col_offset', - 'endline', - 'methods', - 'inner_classes', - 'real_complexity', - ], -) - - -def code2ast(source): - '''Convert a string object into an AST object. - - This function is retained for backwards compatibility, but it no longer - attemps any conversions. It's equivalent to a call to ``ast.parse``. - ''' - return ast.parse(source) - - -class Function(BaseFunc): - '''Object represeting a function block.''' - - @property - def letter(self): - '''The letter representing the function. It is `M` if the function is - actually a method, `F` otherwise. - ''' - return 'M' if self.is_method else 'F' - - @property - def fullname(self): - '''The full name of the function. If it is a method, then the full name - is: - {class name}.{method name} - Otherwise it is just the function name. - ''' - if self.classname is None: - return self.name - return '{0}.{1}'.format(self.classname, self.name) - - def __str__(self): - '''String representation of a function block.''' - return '{0} {1}:{2}->{3} {4} - {5}'.format( - self.letter, - self.lineno, - self.col_offset, - self.endline, - self.fullname, - self.complexity, - ) - - -class Class(BaseClass): - '''Object representing a class block.''' - - letter = 'C' - - @property - def fullname(self): - '''The full name of the class. It is just its name. This attribute - exists for consistency (see :data:`Function.fullname`). - ''' - return self.name - - @property - def complexity(self): - '''The average complexity of the class. It corresponds to the average - complexity of its methods plus one. - ''' - if not self.methods: - return self.real_complexity - methods = len(self.methods) - return int(self.real_complexity / float(methods)) + (methods > 1) - - def __str__(self): - '''String representation of a class block.''' - return '{0} {1}:{2}->{3} {4} - {5}'.format( - self.letter, - self.lineno, - self.col_offset, - self.endline, - self.name, - self.complexity, - ) - - -class CodeVisitor(ast.NodeVisitor): - '''Base class for every NodeVisitors in `radon.visitors`. It implements a - couple utility class methods and a static method. - ''' - - @staticmethod - def get_name(obj): - '''Shorthand for ``obj.__class__.__name__``.''' - return obj.__class__.__name__ - - @classmethod - def from_code(cls, code, **kwargs): - '''Instanciate the class from source code (string object). The - `**kwargs` are directly passed to the `ast.NodeVisitor` constructor. - ''' - return cls.from_ast(code2ast(code), **kwargs) - - @classmethod - def from_ast(cls, ast_node, **kwargs): - '''Instantiate the class from an AST node. The `**kwargs` are - directly passed to the `ast.NodeVisitor` constructor. - ''' - visitor = cls(**kwargs) - visitor.visit(ast_node) - return visitor - - -class ComplexityVisitor(CodeVisitor): - '''A visitor that keeps track of the cyclomatic complexity of - the elements. - - :param to_method: If True, every function is treated as a method. In this - case the *classname* parameter is used as class name. - :param classname: Name of parent class. - :param off: If True, the starting value for the complexity is set to 1, - otherwise to 0. - ''' - - def __init__( - self, to_method=False, classname=None, off=True, no_assert=False - ): - self.off = off - self.complexity = 1 if off else 0 - self.functions = [] - self.classes = [] - self.to_method = to_method - self.classname = classname - self.no_assert = no_assert - self._max_line = float('-inf') - - @property - def functions_complexity(self): - '''The total complexity from all functions (i.e. the total number of - decision points + 1). - - This is *not* the sum of all the complexity from the functions. Rather, - it's the complexity of the code *inside* all the functions. - ''' - return sum(map(GET_COMPLEXITY, self.functions)) - len(self.functions) - - @property - def classes_complexity(self): - '''The total complexity from all classes (i.e. the total number of - decision points + 1). - ''' - return sum(map(GET_REAL_COMPLEXITY, self.classes)) - len(self.classes) - - @property - def total_complexity(self): - '''The total complexity. Computed adding up the visitor complexity, the - functions complexity, and the classes complexity. - ''' - return ( - self.complexity - + self.functions_complexity - + self.classes_complexity - + (not self.off) - ) - - @property - def blocks(self): - '''All the blocks visited. These include: all the functions, the - classes and their methods. The returned list is not sorted. - ''' - blocks = [] - blocks.extend(self.functions) - for cls in self.classes: - blocks.append(cls) - blocks.extend(cls.methods) - return blocks - - @property - def max_line(self): - '''The maximum line number among the analyzed lines.''' - return self._max_line - - @max_line.setter - def max_line(self, value): - '''The maximum line number among the analyzed lines.''' - if value > self._max_line: - self._max_line = value - - def generic_visit(self, node): - '''Main entry point for the visitor.''' - # Get the name of the class - name = self.get_name(node) - # Check for a lineno attribute - if hasattr(node, 'lineno'): - self.max_line = node.lineno - # The Try/Except block is counted as the number of handlers - # plus the `else` block. - # In Python 3.3 the TryExcept and TryFinally nodes have been merged - # into a single node: Try - if name in ('Try', 'TryExcept'): - self.complexity += len(node.handlers) + len(node.orelse) - elif name == 'BoolOp': - self.complexity += len(node.values) - 1 - # Ifs, with and assert statements count all as 1. - # Note: Lambda functions are not counted anymore, see #68 - elif name in ('If', 'IfExp'): - self.complexity += 1 - # The For and While blocks count as 1 plus the `else` block. - elif name in ('For', 'While', 'AsyncFor'): - self.complexity += bool(node.orelse) + 1 - # List, set, dict comprehensions and generator exps count as 1 plus - # the `if` statement. - elif name == 'comprehension': - self.complexity += len(node.ifs) + 1 - - super(ComplexityVisitor, self).generic_visit(node) - - def visit_Assert(self, node): - '''When visiting `assert` statements, the complexity is increased only - if the `no_assert` attribute is `False`. - ''' - self.complexity += not self.no_assert - - def visit_AsyncFunctionDef(self, node): - '''Async function definition is the same thing as the synchronous - one. - ''' - self.visit_FunctionDef(node) - - def visit_FunctionDef(self, node): - '''When visiting functions a new visitor is created to recursively - analyze the function's body. - ''' - # The complexity of a function is computed taking into account - # the following factors: number of decorators, the complexity - # the function's body and the number of closures (which count - # double). - closures = [] - body_complexity = 1 - for child in node.body: - visitor = ComplexityVisitor(off=False, no_assert=self.no_assert) - visitor.visit(child) - closures.extend(visitor.functions) - # Add general complexity but not closures' complexity, see #68 - body_complexity += visitor.complexity - - func = Function( - node.name, - node.lineno, - node.col_offset, - max(node.lineno, visitor.max_line), - self.to_method, - self.classname, - closures, - body_complexity, - ) - self.functions.append(func) - - def visit_ClassDef(self, node): - '''When visiting classes a new visitor is created to recursively - analyze the class' body and methods. - ''' - # The complexity of a class is computed taking into account - # the following factors: number of decorators and the complexity - # of the class' body (which is the sum of all the complexities). - methods = [] - # According to Cyclomatic Complexity definition it has to start off - # from 1. - body_complexity = 1 - classname = node.name - visitors_max_lines = [node.lineno] - inner_classes = [] - for child in node.body: - visitor = ComplexityVisitor( - True, classname, off=False, no_assert=self.no_assert - ) - visitor.visit(child) - methods.extend(visitor.functions) - body_complexity += ( - visitor.complexity - + visitor.functions_complexity - + len(visitor.functions) - ) - visitors_max_lines.append(visitor.max_line) - inner_classes.extend(visitor.classes) - - cls = Class( - classname, - node.lineno, - node.col_offset, - max(visitors_max_lines + list(map(GET_ENDLINE, methods))), - methods, - inner_classes, - body_complexity, - ) - self.classes.append(cls) - - -class HalsteadVisitor(CodeVisitor): - '''Visitor that keeps track of operators and operands, in order to compute - Halstead metrics (see :func:`radon.metrics.h_visit`). - ''' - - # As of Python 3.8 Num/Str/Bytes/NameConstat - # are now in a common node Constant. - types = { - "Num": "n", - "Name": "id", - "Attribute": "attr", - "Constant": "value", - } - - def __init__(self, context=None): - '''*context* is a string used to keep track the analysis' context.''' - self.operators_seen = set() - self.operands_seen = set() - self.operators = 0 - self.operands = 0 - self.context = context - - # A new visitor is spawned for every scanned function. - self.function_visitors = [] - - @property - def distinct_operators(self): - '''The number of distinct operators.''' - return len(self.operators_seen) - - @property - def distinct_operands(self): - '''The number of distinct operands.''' - return len(self.operands_seen) - - def dispatch(meth): - '''This decorator does all the hard work needed for every node. - - The decorated method must return a tuple of 4 elements: - - * the number of operators - * the number of operands - * the operators seen (a sequence) - * the operands seen (a sequence) - ''' - - def aux(self, node): - '''Actual function that updates the stats.''' - results = meth(self, node) - self.operators += results[0] - self.operands += results[1] - self.operators_seen.update(results[2]) - for operand in results[3]: - new_operand = getattr( - operand, self.types.get(type(operand), ''), operand - ) - name = self.get_name(operand) - new_operand = getattr( - operand, self.types.get(name, ""), operand - ) - - self.operands_seen.add((self.context, new_operand)) - # Now dispatch to children - super(HalsteadVisitor, self).generic_visit(node) - - return aux - - @dispatch - def visit_BinOp(self, node): - '''A binary operator.''' - return (1, 2, (self.get_name(node.op),), (node.left, node.right)) - - @dispatch - def visit_UnaryOp(self, node): - '''A unary operator.''' - return (1, 1, (self.get_name(node.op),), (node.operand,)) - - @dispatch - def visit_BoolOp(self, node): - '''A boolean operator.''' - return (1, len(node.values), (self.get_name(node.op),), node.values) - - @dispatch - def visit_AugAssign(self, node): - '''An augmented assign (contains an operator).''' - return (1, 2, (self.get_name(node.op),), (node.target, node.value)) - - @dispatch - def visit_Compare(self, node): - '''A comparison.''' - return ( - len(node.ops), - len(node.comparators) + 1, - map(self.get_name, node.ops), - node.comparators + [node.left], - ) - - def visit_FunctionDef(self, node): - '''When visiting functions, another visitor is created to recursively - analyze the function's body. We also track information on the function - itself. - ''' - func_visitor = HalsteadVisitor(context=node.name) - - for child in node.body: - visitor = HalsteadVisitor.from_ast(child, context=node.name) - self.operators += visitor.operators - self.operands += visitor.operands - self.operators_seen.update(visitor.operators_seen) - self.operands_seen.update(visitor.operands_seen) - - func_visitor.operators += visitor.operators - func_visitor.operands += visitor.operands - func_visitor.operators_seen.update(visitor.operators_seen) - func_visitor.operands_seen.update(visitor.operands_seen) - - # Save the visited function visitor for later reference. - self.function_visitors.append(func_visitor) - - def visit_AsyncFunctionDef(self, node): - '''Async functions are similar to standard functions, so treat them as - such. - ''' - self.visit_FunctionDef(node)