Cercis /ˈsɜːrsɪs/ is a Python code formatter that is more configurable than Black (a popular Python code formatter).
Cercis is also the name of a deciduous tree that boasts vibrant pink to purple-hued flowers, which bloom in early spring.
This code repository is forked from and directly inspired by Black. The original license of Black is included in this repository (see LICENSE_ORIGINAL).
Cercis inherited Black's very comprehensive test cases, which means we are confident that our configurability addition does not introduce any undesirable side effects. We also thoroughly tested every configurable options that we added.
In particular, via its configurable options, Cercis can completely fall back to Black. See Section 4.5 below for more details.
While we like the idea of auto-formatting and code readability, we take issue with some style choices and the lack of configurability of the Black formatter. Therefore, Cercis aims at providing some configurability beyond Black's limited offering.
Cercis can be installed by running pip install cercis
. It requires Python
3.7+ to run. If you want to format Jupyter Notebooks, install with
pip install "cercis[jupyter]"
.
To get started right away with sensible defaults:
cercis {source_file_or_directory}
You can run Cercis as a package if running it as a script doesn't work:
python -m cercis {source_file_or_directory}
The commands above reformat entire file(s) in place.
To format Python files (.py), put the following into your
.pre-commit-config.yaml
file. Remember to replace <VERSION>
with your
version of this tool (such as v0.1.0
):
- repo: https://github.com/jsh9/cercis
rev: <VERSION>
hooks:
- id: cercis
args: [--line-length=88]
To format Jupyter notebooks (.ipynb), put the following into your
.pre-commit-config.yaml
file:
- repo: https://github.com/jsh9/cercis
rev: <VERSION>
hooks:
- id: cercis-jupyter
args: [--line-length=88]
See pre-commit for more instructions. In particular, here is how to specify arguments in pre-commit config.
Cercis's code style is largely consistent with the style of of Black.
The main difference is that Cercis provides several configurable options that Black doesn't. Configurability is our main motivation behind creating Cercis.
Cercis offers the following configurable options:
- Line length
- Single quote vs double quote
- Tabs vs spaces
- Base indentation spaces
- Extra indentation at line continuation
- "Simple" lines with long strings
- Collapse nested brackets
- Wrap pragma comments
The next section (How to configure Cercis) contains detailed instructions of how to configure these options.
Cercis uses 79 characters as the line length limit, instead of 88 (Black's default).
You can override this default if necessary.
Option | |
---|---|
Name | --line-length |
Abbreviation | -l |
Default | 79 |
Black's default | 88 |
Command line usage | cercis -l=120 myScript.py |
pyproject.toml usage |
line-length = 120 under [tool.cercis] |
pre-commit usage |
args: [--line-length=120] |
Cercis uses single quotes ('
) as the default for strings, instead of double
quotes ("
) which is Black's default.
You can override this default if necessary.
Option | |
---|---|
Name | --single-quote |
Abbreviation | -sq |
Default | True |
Black's default | False |
Command line usage | cercis -sq=True myScript.py |
pyproject.toml usage |
single-quote = false under [tool.cercis] |
pre-commit usage |
args: [--single-quote=False] |
Cercis offers users the ability to use tabs rather than spaces.
There are two associated options:
--use-tabs
(bool): whether to use tabs or spaces to format the code
Option | |
---|---|
Name | --use-tabs |
Abbreviation | -tab |
Default | False |
Black's default | False |
Command line usage | cercis -tab=True myScript.py |
pyproject.toml usage |
use-tabs = false under [tool.cercis] |
pre-commit usage |
args: [--use-tabs=False] |
--tab-width
(int): when calculating line length (to determine whether to wrap lines), how wide shall Cercis treat each tab. Only effective when--use-tabs
is set toTrue
.
Option | |
---|---|
Name | --tab-width |
Abbreviation | -tw |
Default | 4 |
Black's default | N/A |
Command line usage | cercis -tab=True -tw=2 myScript.py |
pyproject.toml usage |
tab-width = 2 under [tool.cercis] |
pre-commit usage |
args: [--tab-width=2] |
This option defines the number of spaces that each indentation level adds. This
option has no effect when --use-tabs
is set to True
.
For example, if you set it to 2, contents within a for
block is indented 2
spaces:
for i in (1, 2, 3, 4, 5):
print(i)
Option | |
---|---|
Name | --base-indentation-spaces |
Abbreviation | -bis |
Default | 4 |
Black's default | 4 |
Command line usage | cercis -bis=True -tw=2 myScript.py |
pyproject.toml usage |
base-indentation-spaces = 2 under [tool.cercis] |
pre-commit usage |
args: [--base-indentation-spaces=2] |
There are three associated options:
--function-definition-extra-indent
--other-line-continuation-extra-indent
--closing-bracket-extra-indent
They control whether we add an additional indentation level in some
situations. Note that these options can work well with tabs
(--use-tabs=True
).
# Cercis's default style
def some_function(
arg1_with_long_name: str,
arg2_with_longer_name: int,
arg3_with_longer_name: float,
arg4_with_longer_name: bool,
) -> None:
... |
# Black's style (not configurable)
def some_function(
arg1_with_long_name: str,
arg2_with_longer_name: int,
arg3_with_longer_name: float,
arg4_with_longer_name: bool,
) -> None:
... |
We choose to add an extra indentation level when wrapping a function signature
line. This is because def␣
happens to be 4 characters, so when the base
indentation is 4 spaces, it can be difficult to visually distinguish the
function name and the argument list if we don't add an extra indentation.
If you set --base-indentation-spaces
to other values than 4, this visual
separation issue will disappear, and you may not need to turn this option on.
This style is encouraged in PEP8.
Option | |
---|---|
Name | --function-definition-extra-indent |
Abbreviation | -fdei |
Default | True |
Black's default | False |
Command line usage | cercis -fdei=False myScript.py |
pyproject.toml usage |
function-definition-extra-indent = true under [tool.cercis] |
pre-commit usage |
args: [--function-definition-extra-indent=False] |
"Other line continuations" are cases other than in function definitions, such as:
var = some_function(
arg1_with_long_name,
arg2_with_longer_name,
)
var2 = [
'something',
'something else',
'something more',
]
So if you set this option (--other-line-continuation-extra-indent
) to True
,
you can add an extra level of indentation in these cases:
var = some_function(
arg1_with_long_name,
arg2_with_longer_name,
)
var2 = [
'something',
'something else',
'something more',
]
Option | |
---|---|
Name | --other-line-continuation-extra-indent |
Abbreviation | -olcei |
Default | False |
Black's default | False |
Command line usage | cercis -olcei=True myScript.py |
pyproject.toml usage |
other-line-continuation-extra-indent = true under [tool.cercis] |
pre-commit usage |
args: [----other-line-continuation-extra-indent=False] |
This option lets people customize where the closing bracket should be. Note that both styles are OK according to PEP8.
# --closing-bracket-extra-indent=False
def function(
arg1: int,
arg2: float,
arg3_with_long_name: list,
) -> None:
print('Hello world')
result = func2(
12345,
3.1415926,
[1, 2, 3],
)
something = {
'a': 1,
'b': 2,
'c': 3,
} |
# --closing-bracket-extra-indent=True
def function(
arg1: int,
arg2: float,
arg3_with_long_name: list,
) -> None:
print('Hello world')
result = func2(
12345,
3.1415926,
[1, 2, 3],
)
something = {
'a': 1,
'b': 2,
'c': 3,
} |
Option | |
---|---|
Name | --closing-bracket-extra-indent |
Abbreviation | -cbei |
Default | False |
Black's default | False |
Command line usage | cercis -cbei=True myScript.py |
pyproject.toml usage |
closing-bracket-extra-indent = true under [tool.cercis] |
pre-commit usage |
args: [--closing-bracket-extra-indent=False] |
By default, Black wraps lines that exceed length limit. But for very simple lines (such as assigning a long string to a variable), line wrapping is not necessary.
# Cercis's default style
# (Suppose line length limit is 30 chars)
# Cercis doesn't wrap slightly long lines
var1 = 'This line has 31 chars'
# Cercis doesn't wrap longer lines
var2 = 'This line has 43 characters_______'
# Falls back to Black when comments present
var3 = (
'shorter line' # comment
) |
# Black's style (not configurable)
# (Suppose line length limit is 30 chars)
# Black wraps slightly long lines
var1 = (
"This line has 31 chars"
)
# But Black doesn't wrap longer lines
var2 = "This line has 43 characters_______"
# Black wraps comments like this:
var3 = (
"shorter line" # comment
) |
Option | |
---|---|
Name | --wrap-line-with-long-string |
Abbreviation | -wl |
Default | False |
Black's default | True |
Command line usage | cercis -wl=True myScript.py |
pyproject.toml usage |
wrap-line-with-long-string = true under [tool.cercis] |
pre-commit usage |
args: [--wrap-line-with-long-string=False] |
Cercis by default collapses nested brackets to make the code more compact.
# Cercis's default style
# If line length limit is 30
value = np.array([
[1, 2, 3, 4, 5],
[6, 7, 8, 9, 0],
])
# If line length limit is 10
value = function({
1,
2,
3,
4,
5,
})
|
# Black's style (not configurable)
# If line length limit is 30
value = np.array(
[
[1, 2, 3, 4, 5],
[6, 7, 8, 9, 0],
]
)
# If line length limit is 10
value = function(
{
1,
2,
3,
4,
5,
}
) |
Option | |
---|---|
Name | --collapse-nested-brackets |
Abbreviation | -cnb |
Default | True |
Black's style | False |
Command line usage | cercis -cnb=True myScript.py |
pyproject.toml usage |
collapse-nested-brackets = true under [tool.cercis] |
pre-commit usage |
args: [--collapse-nested-brackets=False] |
The code implementation of this option comes from Pyink, another forked project from Black.
"Pragma comments", in this context, mean the directives for Python linters usually to tell them to ignore certain errors. Pragma comments that Cercis currently recognizes include:
- noqa:
# noqa: E501
- type: ignore:
# type: ignore[no-untyped-def]
- pylint:
# pylint: disable=protected-access
- pytype:
# pytype: disable=attribute-error
# Cercis's default style
# (Suppose line length limit is 30)
# This line has 30 characters
var = some_func(some_long_arg) # noqa:F501
# This line has 31 characters
var_ = some_func(
some_long_arg
) # type: ignore
# Cercis doesn't wraps a line if its main
# content (without the comment) does not
# exceed the line length limit.
|
# Black's style (not configurable)
# (Suppose line length limit is 30)
# Black doesn't wrap lines, no matter
# how long, if the line has
# a "# type: ignore..." comment.
# (This line has 31 characters.)
var_ = some_func(some_long_arg) # type: ignore
# Black does not recognize "# type:ignore",
# even though mypy recognizes it.
var_ = some_func(
some_long_arg
) # type:ignore
# Black only recognizes "# type: ignore"
var_ = some_func(
some_long_arg
) # noqa:F501 |
Option | |
---|---|
Name | --wrap-pragma-comments |
Abbreviation | -wpc |
Default | False |
Black's style | True |
Command line usage | cercis -wpc=True myScript.py |
pyproject.toml usage |
wrap-pragma-comments = true under [tool.cercis] |
pre-commit usage |
args: [--wrap-pragma-comments=False] |
Here are some examples:
cercis --single-quote=True myScript.py
to format files to single quotescercis --function-definition-extra-indent=False myScript.py
to format files without extra indentation at function definitioncercis --line-length=79 myScript.py
to format files with a line length of 79 characters
You can specify the options under the [tool.cercis]
section of the file:
[tool.cercis]
line-length = 88
function-definition-extra-indent = true
single-quote = false
You can specify the options under the args
section of your
.pre-commit-config.yaml
file.
For example:
repos:
- repo: https://github.com/jsh9/cercis
rev: 0.1.0
hooks:
- id: cercis
args: [--function-definition-extra-indent=False, --ling-length=79]
- repo: https://github.com/jsh9/cercis
rev: 0.1.0
hooks:
- id: cercis-jupyter
args: [--function-definition-extra-indent=False, --line-length=79]
The value in rev
can be any Cercis release, or it can be main
, which
means to always use the latest (including unreleased) Cercis features.
Currently, Cercis does not support a config section in tox.ini
. Instead,
you can specify the options in pyproject.toml
.
Here are the configuration options to fall back to Black's behavior. Put them
in pyproject.toml
:
[tool.cercis]
line-length = 88
single-quote = false
use-tabs = false
base-indentation-spaces = 4
function-definition-extra-indent = false
other-line-continuation-extra-indent = false
closing-bracket-extra-indent = false
wrap-line-with-long-string = true
collapse-nested-brackets = false
wrap-pragma-comments = true