From 83e37860d3ead02748efcea39a179b6cd323534f Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 8 Jan 2024 00:15:23 -0700 Subject: [PATCH 01/13] add a new template module to create bmi templates --- src/bmipy/_template.py | 127 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/bmipy/_template.py diff --git a/src/bmipy/_template.py b/src/bmipy/_template.py new file mode 100644 index 0000000..8e2a4df --- /dev/null +++ b/src/bmipy/_template.py @@ -0,0 +1,127 @@ +import inspect +import os +import textwrap + +from bmipy.bmi import Bmi + + +class Template: + """Create template BMI implementations.""" + + def __init__(self, name: str): + self._name = name + self._funcs = dict(inspect.getmembers(Bmi, inspect.isfunction)) + + def render(self) -> str: + """Render a module that defines a class implementing a Bmi.""" + prefix = f"""\ +from __future__ import annotations + +import numpy + +from bmipy.bmi import Bmi + + +class {self._name}(Bmi): +""" + return prefix + (os.linesep * 2).join( + [self._render_func(name) for name in sorted(self._funcs)] + ) + + def _render_func(self, name: str) -> str: + annotations = inspect.get_annotations(self._funcs[name]) + signature = inspect.signature(self._funcs[name], eval_str=False) + + docstring = textwrap.indent( + '"""' + dedent_docstring(self._funcs[name].__doc__) + '"""', " " + ) + + parts = [ + render_function_signature( + name, + tuple(signature.parameters), + annotations, + ), + docstring, + f" raise NotImplementedError({name!r})".replace("'", '"'), + ] + + return textwrap.indent(os.linesep.join(parts), " ") + + +def dedent_docstring(text: str | None, tabsize=4) -> str: + """Dedent a docstring, ignoring indentation of the first line. + + Parameters + ---------- + text : str + The text to dedent. + tabsize : int, optional + Specify the number of spaces to replace tabs with. + + Returns + ------- + str + The dendented string. + """ + if not text: + return "" + + lines = text.expandtabs(tabsize).splitlines(keepends=True) + first = lines[0].lstrip() + try: + body = lines[1:] + except IndexError: + body = [""] + return first + textwrap.dedent("".join(body)) + + +def render_function_signature( + name: str, + params: tuple[str, ...] | None = None, + annotations: dict[str, str] | None = None, + tabsize: int = 4, +) -> str: + """Render a function signature, wrapping if the generated signature is too long. + + Parameters + ---------- + name : str + The name of the function. + params : tuple of str, optional + Names of each of the parameters. + annotations : dict, optional + Annotations for each parameters as well as the return type. + tabsize : int, optional + The number of spacses represented by a tab. + + Returns + ------- + str + The function signature appropriately wrapped. + """ + params = () if params is None else params + annotations = {} if annotations is None else annotations + + prefix = f"def {name}(" + if "return" in annotations: + suffix = f") -> {annotations['return']}:" + else: + suffix = "):" + body = [] + for param in params: + if param in annotations: + param += f": {annotations[param]}" + body.append(param) + + signature = prefix + ", ".join(body) + suffix + if len(signature) <= 88: + return signature + + indent = " " * tabsize + + lines = [prefix, indent + ", ".join(body), suffix] + if max(len(line) for line in lines) <= 88: + return os.linesep.join(lines) + + return os.linesep.join([prefix] + [f"{indent}{line}," for line in body] + [suffix]) From a7d709f06bf9e0b72935276d1691196d7b3f297f Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 8 Jan 2024 00:15:49 -0700 Subject: [PATCH 02/13] simplify; remove jinja, black --- src/bmipy/cmd.py | 89 +++--------------------------------------------- 1 file changed, 4 insertions(+), 85 deletions(-) diff --git a/src/bmipy/cmd.py b/src/bmipy/cmd.py index f167a6b..cafc189 100644 --- a/src/bmipy/cmd.py +++ b/src/bmipy/cmd.py @@ -1,98 +1,17 @@ """Command line interface that create template BMI implementations.""" -import inspect -import keyword -import re - -import black as blk import click -import jinja2 - -from bmipy import Bmi - -BMI_TEMPLATE = """# -*- coding: utf-8 -*- -{% if with_hints -%} -from typing import Tuple -{%- endif %} - -from bmipy import Bmi -import numpy - - -class {{ name }}(Bmi): -{% for func in funcs %} - def {{ func }}{{ funcs[func].sig }}: - \"\"\"{{ funcs[func].doc }}\"\"\" - raise NotImplementedError("{{ func }}") -{% endfor %} -""" - - -def _remove_hints_from_signature(signature): - """Remove hint annotation from a signature.""" - params = [] - for _, param in signature.parameters.items(): - params.append(param.replace(annotation=inspect.Parameter.empty)) - return signature.replace( - parameters=params, return_annotation=inspect.Signature.empty - ) - -def _is_valid_class_name(name): - p = re.compile(r"^[^\d\W]\w*\Z", re.UNICODE) - return p.match(name) and not keyword.iskeyword(name) - - -def render_bmi(name, black=True, hints=True): - """Render a template BMI implementation in Python. - - Parameters - ---------- - name : str - Name of the new BMI class to implement. - black : bool, optional - If True, reformat the source using black styling. - hints : bool, optiona - If True, include type hint annotation. - - Returns - ------- - str - The contents of a new Python module that contains a template for - a BMI implementation. - """ - if _is_valid_class_name(name): - env = jinja2.Environment() - template = env.from_string(BMI_TEMPLATE) - - funcs = {} - for func_name, func in inspect.getmembers(Bmi, inspect.isfunction): - signature = inspect.signature(func) - if not hints: - signature = _remove_hints_from_signature(signature) - funcs[func_name] = {"sig": signature, "doc": func.__doc__} - - contents = template.render(name=name, funcs=funcs, with_hints=hints) - - if black: - contents = blk.format_file_contents( - contents, fast=True, mode=blk.FileMode() - ) - - return contents - else: - raise ValueError(f"invalid class name ({name})") +from bmipy._template import Template @click.command() @click.version_option() -@click.option("--black / --no-black", default=True, help="format output with black") -@click.option("--hints / --no-hints", default=True, help="include type hint annotation") @click.argument("name") @click.pass_context -def main(ctx, name, black, hints): +def main(ctx: click.Context, name: str): """Render a template BMI implementation in Python for class NAME.""" - if _is_valid_class_name(name): - print(render_bmi(name, black=black, hints=hints)) + if name.isidentifier(): + print(Template(name).render()) else: click.secho( f"💥 💔 💥 {name!r} is not a valid class name in Python", From 3b6f3aa8db4c850d628f64fa09a0382c5ad1166d Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 8 Jan 2024 00:16:59 -0700 Subject: [PATCH 03/13] update pre-commit hooks --- .pre-commit-config.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 39861e7..df5d08d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.12.1 hooks: - id: black name: black @@ -23,7 +23,7 @@ repos: additional_dependencies: [".[jupyter]"] - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 + rev: 7.0.0 hooks: - id: flake8 additional_dependencies: @@ -42,19 +42,19 @@ repos: language: python - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.15.0 hooks: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/PyCQA/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort files: \.py$ - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-builtin-literals - id: check-added-large-files @@ -68,7 +68,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/regebro/pyroma - rev: "4.1" + rev: "4.2" hooks: - id: pyroma args: ["-d", "--min=10", "."] @@ -86,7 +86,7 @@ repos: - cython - repo: https://github.com/PyCQA/pydocstyle - rev: 6.1.1 + rev: 6.3.0 hooks: - id: pydocstyle files: bmipy/.*\.py$ @@ -96,7 +96,7 @@ repos: additional_dependencies: [".[toml]"] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.982 + rev: v1.8.0 hooks: - id: mypy additional_dependencies: [types-all] From b5ca9e2ff92359cdda8c717ac6221368bfe91cd4 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 8 Jan 2024 00:24:54 -0700 Subject: [PATCH 04/13] import annotations from the future --- src/bmipy/__init__.py | 1 + src/bmipy/_template.py | 2 ++ src/bmipy/_version.py | 2 ++ src/bmipy/bmi.py | 1 + src/bmipy/cmd.py | 2 ++ 5 files changed, 8 insertions(+) diff --git a/src/bmipy/__init__.py b/src/bmipy/__init__.py index b93094a..13dc3b3 100644 --- a/src/bmipy/__init__.py +++ b/src/bmipy/__init__.py @@ -1,4 +1,5 @@ """The Basic Model Interface (BMI) for Python.""" +from __future__ import annotations from ._version import __version__ from .bmi import Bmi diff --git a/src/bmipy/_template.py b/src/bmipy/_template.py index 8e2a4df..73a4c70 100644 --- a/src/bmipy/_template.py +++ b/src/bmipy/_template.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import inspect import os import textwrap diff --git a/src/bmipy/_version.py b/src/bmipy/_version.py index 90956f3..6ad6d9a 100644 --- a/src/bmipy/_version.py +++ b/src/bmipy/_version.py @@ -1 +1,3 @@ +from __future__ import annotations + __version__ = "2.0.2.dev0" diff --git a/src/bmipy/bmi.py b/src/bmipy/bmi.py index 41ed450..807c67f 100644 --- a/src/bmipy/bmi.py +++ b/src/bmipy/bmi.py @@ -3,6 +3,7 @@ This language specification is derived from the Scientific Interface Definition Language (SIDL) file `bmi.sidl `_. """ +from __future__ import annotations from abc import ABC, abstractmethod diff --git a/src/bmipy/cmd.py b/src/bmipy/cmd.py index cafc189..7b92646 100644 --- a/src/bmipy/cmd.py +++ b/src/bmipy/cmd.py @@ -1,4 +1,6 @@ """Command line interface that create template BMI implementations.""" +from __future__ import annotations + import click from bmipy._template import Template From 8d4c3d26dd45ec22755e73b207f7387c38c04579 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 8 Jan 2024 00:26:17 -0700 Subject: [PATCH 05/13] remove jinja, black from dependencies --- pyproject.toml | 2 -- requirements.in | 2 -- requirements/requires.txt | 2 -- 3 files changed, 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 106f8cc..8da82e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,9 +27,7 @@ classifiers = [ ] requires-python = ">=3.9" dependencies = [ - "black", "click", - "jinja2", "numpy", ] dynamic = ["version"] diff --git a/requirements.in b/requirements.in index 666203b..30fca5f 100644 --- a/requirements.in +++ b/requirements.in @@ -1,4 +1,2 @@ -black click -jinja2 numpy diff --git a/requirements/requires.txt b/requirements/requires.txt index 10b006d..eeae57b 100644 --- a/requirements/requires.txt +++ b/requirements/requires.txt @@ -1,4 +1,2 @@ -black==23.10.1 click==8.1.7 -jinja2==3.1.2 numpy==1.26.3 From 120458e3039a59d811e6f81e88919608d8f6b070 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 8 Jan 2024 00:33:12 -0700 Subject: [PATCH 06/13] remove tests from black and hints cli options --- tests/test_cli.py | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 839758f..f0af91e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -41,35 +41,11 @@ def test_cli_default(tmpdir): def test_cli_with_hints(tmpdir): runner = CliRunner() with tmpdir.as_cwd(): - result = runner.invoke(main, ["MyBmiWithHints", "--hints"]) + result = runner.invoke(main, ["MyBmiWithHints"]) assert result.exit_code == 0 assert "->" in result.output -def test_cli_without_hints(tmpdir): - runner = CliRunner() - with tmpdir.as_cwd(): - result = runner.invoke(main, ["MyBmiWithoutHints", "--no-hints"]) - assert result.exit_code == 0 - assert "->" not in result.output - - -def test_cli_with_black(tmpdir): - runner = CliRunner() - with tmpdir.as_cwd(): - result = runner.invoke(main, ["MyBmiWithHints", "--black"]) - assert result.exit_code == 0 - assert max([len(line) for line in result.output.splitlines()]) <= 88 - - -def test_cli_without_black(tmpdir): - runner = CliRunner() - with tmpdir.as_cwd(): - result = runner.invoke(main, ["MyBmiWithoutHints", "--no-black"]) - assert result.exit_code == 0 - assert max([len(line) for line in result.output.splitlines()]) > 88 - - @pytest.mark.parametrize("bad_name", ["True", "0Bmi"]) def test_cli_with_bad_class_name(tmpdir, bad_name): runner = CliRunner() From 3357830ff938020f9ef041475dd76fe098e8d09f Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 8 Jan 2024 00:38:48 -0700 Subject: [PATCH 07/13] fix test for valid class name --- src/bmipy/cmd.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bmipy/cmd.py b/src/bmipy/cmd.py index 7b92646..21837d7 100644 --- a/src/bmipy/cmd.py +++ b/src/bmipy/cmd.py @@ -1,6 +1,8 @@ """Command line interface that create template BMI implementations.""" from __future__ import annotations +import keyword + import click from bmipy._template import Template @@ -12,7 +14,7 @@ @click.pass_context def main(ctx: click.Context, name: str): """Render a template BMI implementation in Python for class NAME.""" - if name.isidentifier(): + if name.isidentifier() and not keyword.iskeyword(name): print(Template(name).render()) else: click.secho( From 1ba9ece59ae0ed5d87592450f5ad942ad0d65afd Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 8 Jan 2024 00:41:54 -0700 Subject: [PATCH 08/13] remove python 3.9 tests --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ec14ef7..b106b80 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 @@ -51,7 +51,7 @@ jobs: - uses: actions/checkout@v4 - uses: wntrblm/nox@2023.04.22 with: - python-versions: "3.9" + python-versions: "3.12" - name: Lint run: nox --non-interactive --error-on-missing-interpreter --session "lint" From d9f206fc430eb883b37bea75c8703c056c76c120 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 8 Jan 2024 10:32:38 -0700 Subject: [PATCH 09/13] add tests for dedenting docstrings --- tests/test_template.py | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/test_template.py diff --git a/tests/test_template.py b/tests/test_template.py new file mode 100644 index 0000000..978dbd0 --- /dev/null +++ b/tests/test_template.py @@ -0,0 +1,48 @@ +import pytest + +from bmipy._template import dedent_docstring + + +@pytest.mark.parametrize( + "text", + ( + " Foo", + "\tFoo\n bar ", + "\tFoo\n bar", + "Foo\n bar\n baz\n", + ), +) +def test_dedent_docstring_aligned(text): + fixed_text = dedent_docstring(text) + assert [l.lstrip() for l in fixed_text.splitlines()] == fixed_text.splitlines() + + +@pytest.mark.parametrize( + "text", ("Foo", " Foo", "\tFoo ", "\n Foo", " Foo\nBar\nBaz") +) +def test_dedent_docstring_lstrip_first_line(text): + fixed_text = dedent_docstring(text) + assert fixed_text[0].lstrip() == fixed_text[0] + + +@pytest.mark.parametrize("text", (None, "", """""")) +def test_dedent_docstring_empty(text): + assert dedent_docstring(text) == "" + + +@pytest.mark.parametrize( + "text,tabsize", + (("\tFoo", 8), ("Foo\n\tBar baz", 2), ("\t\tFoo\tBar\nBaz", 0)), +) +def test_dedent_docstring_tabsize(text, tabsize): + fixed_text = dedent_docstring(text, tabsize) + assert [l.lstrip() for l in fixed_text.splitlines()] == fixed_text.splitlines() + + +@pytest.mark.parametrize( + "text", + ("Foo\n Bar\n Baz",), +) +def test_dedent_docstring_body_is_left_justified(text): + lines = dedent_docstring(text).splitlines()[1:] + assert any(line.lstrip() == line for line in lines) From 1ffa253c0c085eed8db01a6a8396610eb9d3c29a Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 8 Jan 2024 14:06:10 -0700 Subject: [PATCH 10/13] add tests for generated function code --- tests/test_template.py | 49 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/tests/test_template.py b/tests/test_template.py index 978dbd0..b4c7502 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -1,6 +1,6 @@ import pytest -from bmipy._template import dedent_docstring +from bmipy._template import dedent_docstring, render_function_signature @pytest.mark.parametrize( @@ -14,7 +14,9 @@ ) def test_dedent_docstring_aligned(text): fixed_text = dedent_docstring(text) - assert [l.lstrip() for l in fixed_text.splitlines()] == fixed_text.splitlines() + assert [ + line.lstrip() for line in fixed_text.splitlines() + ] == fixed_text.splitlines() @pytest.mark.parametrize( @@ -36,7 +38,9 @@ def test_dedent_docstring_empty(text): ) def test_dedent_docstring_tabsize(text, tabsize): fixed_text = dedent_docstring(text, tabsize) - assert [l.lstrip() for l in fixed_text.splitlines()] == fixed_text.splitlines() + assert [ + line.lstrip() for line in fixed_text.splitlines() + ] == fixed_text.splitlines() @pytest.mark.parametrize( @@ -46,3 +50,42 @@ def test_dedent_docstring_tabsize(text, tabsize): def test_dedent_docstring_body_is_left_justified(text): lines = dedent_docstring(text).splitlines()[1:] assert any(line.lstrip() == line for line in lines) + + +@pytest.mark.parametrize( + "annotations", + ( + {"bar": "int"}, + {"bar" * 10: "int", "baz" * 10: "int"}, + {"bar" * 20: "int", "baz" * 20: "int"}, + ), +) +def test_render_function_wraps(annotations): + params = list(annotations) + annotations["return"] = "str" + + text = render_function_signature("foo", params, annotations) + assert max(len(line) for line in text.splitlines()) <= 88 + + +@pytest.mark.parametrize( + "annotations", + ( + {}, + {"bar": "int"}, + {"bar" * 10: "int", "baz" * 10: "int"}, + {"bar" * 20: "int", "baz" * 20: "int"}, + ), +) +def test_render_function_is_valid(annotations): + params = list(annotations) + annotations["return"] = "str" + + text = render_function_signature("foo", params, annotations) + generated_code = f"{text}\n return 'FOOBAR!'" + + globs = {} + exec(generated_code, globs) + + assert "foo" in globs + assert globs["foo"](*range(len(params))) == "FOOBAR!" From 1d5dbd15f01016877f9b4bc4f7e52342e5ac2ece Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 8 Jan 2024 14:14:45 -0700 Subject: [PATCH 11/13] add width keyword for the max length of a line --- src/bmipy/_template.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/bmipy/_template.py b/src/bmipy/_template.py index 73a4c70..981131e 100644 --- a/src/bmipy/_template.py +++ b/src/bmipy/_template.py @@ -19,7 +19,7 @@ def render(self) -> str: prefix = f"""\ from __future__ import annotations -import numpy +import numpy as np from bmipy.bmi import Bmi @@ -43,6 +43,7 @@ def _render_func(self, name: str) -> str: name, tuple(signature.parameters), annotations, + width=84, ), docstring, f" raise NotImplementedError({name!r})".replace("'", '"'), @@ -83,6 +84,7 @@ def render_function_signature( params: tuple[str, ...] | None = None, annotations: dict[str, str] | None = None, tabsize: int = 4, + width: int = 88, ) -> str: """Render a function signature, wrapping if the generated signature is too long. @@ -117,13 +119,13 @@ def render_function_signature( body.append(param) signature = prefix + ", ".join(body) + suffix - if len(signature) <= 88: + if len(signature) <= width: return signature indent = " " * tabsize lines = [prefix, indent + ", ".join(body), suffix] - if max(len(line) for line in lines) <= 88: + if max(len(line) for line in lines) <= width: return os.linesep.join(lines) return os.linesep.join([prefix] + [f"{indent}{line}," for line in body] + [suffix]) From a54b927b80de9f782d347756bebe9759f36f585a Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 8 Jan 2024 14:15:23 -0700 Subject: [PATCH 12/13] test that generated lines are less than 88 --- tests/test_cli.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index f0af91e..f31ef22 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,3 +1,4 @@ +import importlib import sys import pytest @@ -24,9 +25,6 @@ def test_cli_help(): sys.platform == "win32", reason="See https://github.com/csdms/bmi-python/issues/10" ) def test_cli_default(tmpdir): - import importlib - import sys - runner = CliRunner() with tmpdir.as_cwd(): result = runner.invoke(main, ["MyBmi"]) @@ -38,6 +36,17 @@ def test_cli_default(tmpdir): assert "MyBmi" in mod.__dict__ +@pytest.mark.skipif( + sys.platform == "win32", reason="See https://github.com/csdms/bmi-python/issues/10" +) +def test_cli_wraps_lines(tmpdir): + runner = CliRunner() + with tmpdir.as_cwd(): + result = runner.invoke(main, ["MyBmi"]) + assert result.exit_code == 0 + assert max(len(line) for line in result.output.splitlines()) <= 88 + + def test_cli_with_hints(tmpdir): runner = CliRunner() with tmpdir.as_cwd(): From 4ad1ff165e82cd4b51fd91391d52cde8241114b7 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Mon, 8 Jan 2024 14:22:32 -0700 Subject: [PATCH 13/13] add description of width parameter --- src/bmipy/_template.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bmipy/_template.py b/src/bmipy/_template.py index 981131e..b12907e 100644 --- a/src/bmipy/_template.py +++ b/src/bmipy/_template.py @@ -98,6 +98,8 @@ def render_function_signature( Annotations for each parameters as well as the return type. tabsize : int, optional The number of spacses represented by a tab. + width : int, optional + The maximum width of a line. Returns -------