Plugins
Plugins are the providers of the basic units for the application of rules, defining actions which can be either verbs or conditionals.
conditionals filter the execution of verbs in a rule. If all the conditionals of a rule returns
true
, the verbs are executed. Conditionals are identified because they start with the prefixif
.verbs execute actions against the files defined in the special
files
property of each rule. They act like asserters.
inclusion
Files content inclusion management.
includeLines
Check that the files include expected lines.
Accept an array of strings as the lines to exclude or an array of arrays with the content to exclude and the fixer query as two strings.
Examples
Appends lines not already present in the file to the end.
{
rules: [
files: [".gitignore"],
includeLines: ["venv*/", "/dist/"]
]
}
You can define manually a JMESPath fix query if the line is an array with the line and the fix query as items:
{
rules: [
{
files: [".gitignore"],
hint: "The line 'dist/' must be included in .gitignore",
includeLines: [
// Include all the lines which value is not a set of variants of `dist/`
// and append the line `dist/` to the end of the file
["dist/", "op([?!contains(['/dist/', 'dist', 'dist/'], @)], '+', ['dist/'])"],
"__pycache__/",
]
}
]
}
The returned value of the JMESPath fix query will be the new content
of the file, an array with a '\n'.join(lines)
transformation of the
array of strings that represent the lines of the file.
New in version 0.1.0.
Changed in version 0.7.0: Accept arrays of [line, fixer_query]
as items of the array
to edit manually the files using JMESPath queries.
ifIncludeLines
Conditional to exclude rule only if some files include a set of lines.
If one file don’t include all lines passed as parameter, the rule will be ignored.
Accept an object mapping files to lines that must be included in order to execute the rule.
Example
If the license defined in the LICENSE file is BSD-3, project.license
must correspont:
{
rules: [
files: ["pyproject.toml"],
ifIncludeLines: {
LICENSE: ["BSD 3-Clause License"],
},
JMESPathsMatch: [
["project.license", "BSD-3-License"],
]
]
}
New in version 0.1.0.
includeContent
Check that the files include certain contents.
Accept an array of strings as the contents to include or an array of arrays with the content to include and the fixer query as two strings.
The specified partial contents can match multiple lines and line ending characters. It just raises errors if the passed contents are substrings of each file content.
Example
{
rules: [
files: ["setup.py"],
includeContent: [
'Installation using setup.py is no longer supported.',
]
]
}
New in version 0.8.0.
excludeLines
Check that the files do not include certain lines.
Accept an array of strings as the lines to exclude or an array of arrays with the line to exclude and the fixer query as two strings.
Example
{
rules: [
hint: 'Keep the .vscode folder',
files: ['.gitignore'],
excludeLines: [
'.vscode'
'.vscode/',
'/.vscode',
'/.vscode/',
]
]
}
New in version 0.8.0.
excludeContent
Check that the files do not include certain content.
Accept an array of strings as the contents to exclude or an array of arrays with the content to exclude and the fixer query as two strings.
The specified partial contents can match multiple lines and line ending characters. It just raises errors if the passed contents are substrings of each file content.
Example
Don’t allow code blocks in RST documentation files:
Bash is not a POSIX compliant shell, use Shell lexer.
Pygments’ JSON5 lexer is not implemented yet, use Javascript lexer.
{
rules: [
{
files: ["file.rst"],
excludeContent: [
[
".. code-block:: ",
"map(&replace(@, 'code-block:: ', 'code-block:: '), @)",
],
[
".. code-block:: bash",
"map(&replace(@, 'code-block:: bash', 'code-block:: sh'), @)",
],
[
".. code-block:: json5",
"map(&replace(@, 'code-block:: json5', 'code-block:: js'), @)",
],
],
},
],
}
New in version 0.3.0.
Changed in version 0.7.0: Accept an array ['content-to-exclude', 'fixer-query']
for each item
in the array to perform editions in the file if the content is found.
existence
Check existence of files.
ifFilesExist
Check if a set of files and/or directories exists.
Accept an array of paths. If a path ends with /
character it is
considered a directory.
Examples
If the directory src/ exists, a pyproject.toml file must exist also:
{
rules: [
files: ["pyproject.toml"],
ifFilesExist: ["src/"],
]
}
If the file .pre-commit-hooks.yaml exists, must be declared as an array:
{
rules: [
files: [".pre-commit-hooks.yaml"],
ifFilesExist: [".pre-commit-hooks.yaml"],
JMESPathsMatch: [["type(@)", "array"]]
]
}
New in version 0.4.0.
jmespath
JMES paths manipulation against files.
The actions of this plugin operates against object-serialized versions of files, so only files that can be serialized can be targetted (see Objects serialization).
You can use in expressions all JMESPath builtin functions plus a set of convenient functions defined by the plugin internally:
Standard JMESPath functions
The next functions extends those functions that the official JMESPath library has accepted and are compatible, but they offer some extra features:
- starts_with(search: str, prefix: str[, start: int=0[, end: int=-1]]) bool
Return
true
if string starts with the prefix, otherwise returnfalse
. The argumentprefix
can also be an array of prefixes to look for. With optionalstart
, test string beginning at that position. With optionalend
, stop comparing string at that position.In the official implementation of JMESPath, the
start
andend
parameters are not included andprefix
can only be a string.New in version 0.1.0.
Changed in version 0.7.6:
Added
start
andend
parameters.Added support for
prefix
to be an array of prefixes.
- ends_with(search: str, suffix: str[, start: int=0[, end: int=-1]]) bool
Return
true
if the string ends with the specified suffix, otherwise returnfalse
. The argumentsuffix
can also be a tuple of suffixes to look for. With optionalstart
, test beginning at that position. With optionalend
, stop comparing at that position.In the official implementation of JMESPath, the
start
andend
parameters are not included andsuffix
can only be a string.New in version 0.1.0.
Changed in version 0.7.6:
Added
start
andend
parameters.Added support for
suffix
to be an array of suffixes.
Regex functions
All functions whose name start with regex_
are regex functions, which
always takes the regex to apply as the first parameter following the Python’s
regex standard library syntax.
- regex_match(pattern: str, string: str[, flags: int=0]) bool
Match a regular expression against a string using the Python’s built-in
re.match()
function.New in version 0.1.0.
Changed in version 0.5.0: Allow to pass
flags
optional argument as an integer.
- regex_matchall(pattern: str, strings: list[str]) bool
Match a regular expression against a set of strings defined in an array using the Python’s built-in
re.match()
function.New in version 0.1.0.
Deprecated since version 0.4.0.
- regex_search(pattern: str, string: str[, flags: int=0]) list[str]
Search using a regular expression against a string using the Python’s built-in
re.search()
function. Returns all found groups in an array or an array with the full match as the unique item if no groups are defined. If no results are found, returns an empty array.New in version 0.1.0.
Changed in version 0.5.0: Allow to pass
flags
optional argument as an integer.
- regex_sub(pattern: str, repl: str, string: str[, count: int=0[, flags: int=0]]) str
Replace using a regular expression against a string using the Python’s built-in
re.sub()
function.New in version 0.5.0.
- regex_escape(pattern: str) str
Escape a regular expression pattern using the Python’s built-in
re.escape()
function.New in version 0.7.5.
Utility functions
- os() str
Return the result of the Python’s variable sys.platform.
New in version 0.7.0.
- setenv(envvar: str, value: str | None) dict
Set the value of an environment variable. If you set the value to
null
, the environment variable will be removed.Return the updated environment object.
New in version 0.7.2.
- rootdir_name() str
Returns the name if the root directory of the project, that will be the current working directory or other that could be either passed in project-config --rootdir CLI option or defined in
cli.rootdir
configuration option.New in version 0.6.0.
- op(source: any, operation: str, target: any[, operation: str, target: any]...) any
Apply the operator
operation
between the valuessource
andtarget
using the operators defined in the Python standard library moduleop
for two or more values.The next operators are available:
<=
:operator.le()
==
:operator.eq()
!=
:operator.ne()
>=
:operator.ge()
is
:operator.is_()
is_not
:operator.is_not()
is-not
:operator.is_not()
is not
:operator.is_not()
isNot
:operator.is_not()
and
:operator.and_()
or
:operator.or_()
**
:operator.pow()
count_of
:operator.countOf()
count of
:operator.countOf()
count-of
:operator.countOf()
countOf
:operator.countOf()
index_of
:operator.indexOf()
index of
:operator.indexOf()
index-of
:operator.indexOf()
indexOf
:operator.indexOf()
If
source
andtarget
are both of type array and the operator is one of the next ones, the arrays are converted toset
before applying the operator:<=
:operator.le()
>=
:operator.ge()
and
:operator.and_()
or
:operator.or_()
Example
The next example checks that the configuration field
tool.isort.sections
is a superset or equal to the array of strings['STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER']
appyling the operator<=
.These comparations are easier to do than checking every item in the array with the built-in JMESPath function
contains()
.{ rules: [ { files: ["pyproject.toml"], JMESPathsMatch: [ ["type(tool.isort)", "object"], ["type(tool.isort.sections)", "array"], [ "op(['STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'], '<=', tool.isort.sections)", true, ], ], }, ], }
You can pass multiple operators and values after the
target
argument chaining the operation with multiple operators. For example:op(`5`, '+', `3`, '-', `4`)
New in version 0.1.0.
Changed in version 0.4.0: Convert to
set
before applying operators if both arguments are arrays.Changed in version 0.7.4: Multiple optional operator and values can be passed as positional arguments.
- shlex_split(cmd_str: str) list
Split a string using the Python’s built-in function
shlex.split()
.New in version 0.4.0.
- shlex_join(cmd_list: list[str]) str
Join a list of strings using the Python’s built-in function
shlex.join()
.New in version 0.4.0.
- round(number: float[, precision: int]) float
Round a number to a given precision using the Python’s built-in function
round()
.New in version 0.5.0.
- range([start: float, ]stop: float[, step: float]) list
Return an array of numbers from
start
tostop
with a step ofstep
casting the result of the constructorrange
to an array.New in version 0.5.0.
- count(value: str | list, sub: any[, start: int[, end: int]]) int
Return the number of occurrences of
sub
invalue
usingstr.count()
. Ifstart
andend
are given, return the number of occurrences betweenstart
andend
.New in version 0.5.0.
- find(string: str | list, sub: any[, start: int[, end: int]]) int
Return the lowest index in
value
where subvaluesub
is found. Ifstart
andend
are given, return the number of occurrences betweenstart
andend
. If not found,-1
is returned.If
value
is a string it uses internally the Python’s built-in functionstr.find()
. Ifvalue
is an array, uses the methodstr.index()
.New in version 0.5.0.
- isalnum(string: str) bool
Return True if all characters in
string
are alphanumeric usingstr.isalnum()
.New in version 0.5.0.
- isalpha(string: str) bool
Return True if all characters in
string
are alphabetic usingstr.isalpha()
.New in version 0.5.0.
- isascii(string: str) bool
Return True if all characters in
string
are ASCII usingstr.isascii()
.New in version 0.5.0.
- isdecimal(string: str) bool
Return True if all characters in
string
are decimal usingstr.isdecimal()
.New in version 0.5.0.
- isdigit(string: str) bool
Return True if all characters in
string
are digits usingstr.isdigit()
.New in version 0.5.0.
- isidentifier(string: str) bool
Return True if all characters in
string
are identifiers if the string is a valid identifier according to the Python language definition usingstr.isidentifier()
.New in version 0.5.0.
- islower(string: str) bool
Return True if all characters in
string
are lowercase usingstr.islower()
.New in version 0.5.0.
- isnumeric(string: str) bool
Return True if all characters in
string
are numeric usingstr.isnumeric()
.New in version 0.5.0.
- isprintable(string: str) bool
Return True if all characters in
string
are printable usingstr.isprintable()
.New in version 0.5.0.
- isspace(string: str) bool
Return True if all characters in
string
are whitespace usingstr.isspace()
.New in version 0.5.0.
- istitle(string: str) bool
Return True if all characters in
string
are titlecased usingstr.istitle()
.New in version 0.5.0.
- isupper(string: str) bool
Return True if all characters in
string
are uppercase usingstr.isupper()
.New in version 0.5.0.
- lower(string: str) str
Return a lowercased version of the string using
str.lower()
.New in version 0.5.0.
- lstrip(string: str[, chars: str]) str
Return a left-stripped version of the string using
str.lstrip()
.New in version 0.5.0.
- partition(string: str, sep: str) list[str]
Return an array of 3 items containing the part before the separator, the separator itself, and the part after the separator.
New in version 0.5.0.
- rfind(string: str | list, sub: any[, start: int[, end: int]]) int
Return the highest index in
value
where subvaluesub
is found. Ifstart
andend
are given, return the number of occurrences betweenstart
andend
. If not found,-1
is returned. Ifvalue
is a string it uses internally the Python’s built-in functionstr.find()
orstr.index()
ifvalue
is an array.New in version 0.5.0.
- rpartition(string: str, sep: str) list[str]
Return an array of 3 items containing the part after the separator, the separator itself, and the part before the separator splitting the string at the last occurrence of
sep
.New in version 0.5.0.
- rsplit(string: str[, sep: str[, maxsplit: int]]) list[str]
Return a list of the words in the string, using
sep
as the delimiter string as returned from the methodstr.rsplit()
. Except for splitting from the right,rsplit()
behaves likesplit()
.New in version 0.5.0.
- split(string: str[, sep: str[, maxsplit: int]]) list[str]
Return a list of the words in the string, using
sep
as the delimiter string as returned from the methodstr.split()
. Ifsep
is not given, it defaults toNone
, meaning that any whitespace string is a separator.New in version 0.5.0.
- splitlines(string: str[, keepends: bool]) list[str]
Return a list of the lines in the string, breaking at line boundaries using the method
str.splitlines()
.New in version 0.5.0.
- swapcase(string: str) str
Return a swapped-case version of the string using
str.swapcase()
.New in version 0.5.0.
- title(string: str) str
Return a titlecased version of the string using
str.title()
.New in version 0.5.0.
- upper(string: str) str
Return an uppercased version of the string using
str.upper()
.New in version 0.5.0.
- zfill(string: str, width: int) str
Return a zero-padded version of the string using
str.zfill()
.New in version 0.5.0.
- enumerate(string: str | list | dict) list[list[int, str]]
Return an array of arrays containing the index and value of each item in the iterable. If the iterable is an object, the value is converted before using
to_items()
.New in version 0.5.0.
- to_items(string: dict) list[list[str, any]]
Convert an object to an array of arrays containing the key and value of each item.
New in version 0.5.0.
- from_items(items: list[list[str, any]]) dict
Convert an array of arrays containing the key and value of each item to an object.
New in version 0.5.0.
File system functions
The next functions are useful for file system operations.
- isfile(path: str) bool
Return
true
if the givenpath
is a file,false
otherwise.New in version 0.8.0.
- isdir(path: str) bool
Return
true
if the givenpath
is a directory,false
otherwise.New in version 0.8.0.
- exists(path: str) bool
Return
true
if the givenpath
exists,false
otherwise.New in version 0.8.0.
- listdir(path: str) list[str] | null
Return a list of the files and directories in the given
path
. If thepath
does not exist, returnnull
.New in version 0.8.0.
- mkdir(path: str) bool
Create a directory in the given
path
. Returntrue
if the directory has been created,false
if it already exists.New in version 0.8.0.
- rmdir(path: str) bool
Remove the directory in the given
path
. Returntrue
if the directory has been removed,false
if it does not exist.New in version 0.8.0.
- glob(pattern: str[, recursive: bool = false]) list[str]
Return a list of the files and directories in the given
path
using theglob
module. If recursive is true, the pattern**
will match any files and zero or more directories, subdirectories and symbolic links to directories.New in version 0.8.0.
Updater functions
The next functions take an value as the first argument, make some update
on this base
object and return it updated. Useful for fix queries when
you need to return fixed contents for files.
- update(base: dict, next: dict) dict
Update the
base
object with thenext
object using Python’s builtindict.update()
.Returns the updated
base
object.New in version 0.7.0.
- insert(base: list, index: int, item: t.Any) list
Insert a
item
at the givenindex
in the arraybase
.Returns the updated
base
array.New in version 0.7.0.
- deepmerge(base: dict, next: dict, strategy: str | list = 'conservative_merger') dict
Merge the
base
andnext
objects using the librarydeepmerge
.Returns the updated
base
object.The argument of
strategy
controls how the objects are merged. It can accept strings with deepmerge strategy names:Example
deepmerge(@, `{"foo": "bar"}`, 'always_merger')
Or an array with 3 values, the same that takes the class
deepmerge.merger.Merger
as arguments:type_strategies
fallback_strategies
type_conflict_strategies
Example
deepmerge( @, `{"foo": ["bar"]}`, `[[["list", "append"], ["dict": "merge"]], ["override"], ["override"]]` )
New in version 0.7.0.
- set(base: dict, key: str, value: t.Any) dict
Set the value of the
key
in thebase
object tovalue
.Returns the updated
base
object.New in version 0.7.0.
- unset(base: dict, key: str) dict
If has it, remove the
key
from thebase
object.Returns the updated
base
object.New in version 0.7.0.
- replace(base: str, old: str, new: str[, count: int | None = None])
Replace the
old
string with thenew
string in thebase
string using the Python’s built-in string methodstr.replace()
.Returns the updated
base
string.New in version 0.7.0.
- removeprefix(string: str, prefix: str) str
Return a string with the given prefix removed using
str.removeprefix()
.New in version 0.5.0.
- removesuffix(string: str, suffix: str) str
Return a string with the given suffix removed using
str.removesuffix()
.New in version 0.5.0.
- format(schema: str, *args: any) str
Return a string formatted using the Python’s built-in
format()
function. The variableschema
only accepts numeric indexes delimited by braces{}
for positional arguments in*args
.New in version 0.5.0.
- strip(string: str[, chars: str]) str
Return a stripped version of the string using
str.strip()
.New in version 0.5.0.
- rstrip(string: str[, chars: str]) str
Return a right-stripped version of the string using
str.rstrip()
.New in version 0.5.0.
- capitalize(string: str) str
Capitalize the first letter of a string using
str.capitalize()
.New in version 0.5.0.
- casefold(string: str) str
Return a casefolded copy of a string using
str.casefold()
.New in version 0.5.0.
- center(string: str, width: int[, fillchar: str]) str
Return centered in a string of length
width
usingstr.center()
.New in version 0.5.0.
- ljust(string: str, width: int[, fillchar: str]) str
Return a left-justified version of the string using
str.ljust()
.New in version 0.5.0.
- rjust(string: str, width: int[, fillchar: str]) str
Return a right-justified version of the string using
str.rjust()
.New in version 0.5.0.
Github functions
The functions which name starts with gh_ are functions that connect to only Github sources.
- gh_tags(repo_owner: str, repo_name: str[, only_semver: bool = False]) list[str]
Return the list of tags of the Github repository, ordered from latest to oldest.
If you pass the third parameter as a
true
value, only the tags that are following semantic versioning (even if they are prepended with some text likev
) will be returned.This function is really useful setting the
rev
properties in .pre-commit-config.yaml files.New in version 0.7.1.
Fix queries
The verbs of the jmespath plugin can fix files by applying a JMESPath query over the previous content of the files. The fix-queries arguments are always optional.
If no fix-query is provided, project-config will attempt to build an expected node tree instance to update the content parsing the other queries arguments and the expected value.
The query will be a syntax like (example merging objects):
deepmerge(@, `{ "foo": "bar" }`)
Where @
is the previous content of the file.
The result from this JMESPath expression will be the next content of the file.
So these transformer functions like deepmerge()
, insert()
or
update()
allow you to edit your files with total flexibility.
Automatic fixes
Queries that are simple can be automatically fixed by the plugin. For example, a constant query with their expected value:
{
"name": "my-project"
}
{
"name": "my-project",
"license": "BSD-3-Clause"
}
{
files: ["package.json"],
JMESPathsMatch: [["license", "BSD-3-Clause"]]
}
Currently, is possible to automatically fix the following cases:
Query to constant.
Query to constant in nested objects.
Expression using the
type
function liketype(foo.bar)
with expected value as'array'
(creates{foo: {bar: []}}
nodes if doesn’t exists before).Indexed expressions with indexes like
type(foo[0].bar)
with expected value as'object'
(prepends,{bar: {}}
to the arraybar
, creating it if does not exists).Forbidden key in root object like
contains(keys(@), 'foo')
with expected value asfalse
(removes the keyfoo
from the root object).
JMESPathsMatch
Compares a set of JMESPath expressions against results.
Object-serializes each file in the files
property of the rule
and executes each expression given in the first item of the
tuples passed as value. If a result don’t match, report an error.
Example
The .editorconfig file must have the next content:
root = true
[*]
end_of_line = lf
charset = utf-8
indent_style = space
trim_trailing_whitespace = true
{
rules: [
{
files: [".editorconfig"],
JMESPathsMatch: [
['"".root', true],
['"*".end_of_line', "lf"],
['"*".indent_style', "space"],
['"*".charset', "utf-8"],
['"*".trim_trailing_whitespace', true],
],
}
]
}
New in version 0.1.0.
Changed in version 0.7.0: The verb also accepts fix queries as third item of rows.
crossJMESPathsMatch
JMESPaths matching between multiple files.
Accept an array of arrays. Each one of these arrays must have the syntax:
[
"filesJMESPathExpression", // expression to query each file in `files` property of the rule
["otherFile.ext", "JMESPathExpression"]..., // optionally other files
"finalJMESPathExpression", // an array with results of previous expressions as input
expectedValue, // value to compare with the result of final JMESPath expression
]
The executed steps are:
For each object-serialized file in
files
property of the rule.Execute
"filesJMESPathExpression"
and append the result to a temporal array.For each pair of
["otherFile.ext", "JMESPathExpression"]
, execute"JMESPathExpression"
against the object-serialized version of"otherFile.ext"
and append each result to the temporal array.Execute
"finalJMESPathExpression"
against the temporal array.Compare the final result with
expectedValue
and raise error if not match.
Tip
Other file paths can be URLs if you want to match against online sources.
Example
The release
field of a Sphinx configuration defined in a file
docs/conf.py must be the same that the version of the project metadata
defined in th file pyproject.toml, field project.version
:
{
rules: [
{
files: ["pyproject.toml"],
crossJMESPathsMatch: [
[
"project.version",
["docs/conf.py", "release"],
"op([0], '==', [1])",
true,
],
],
hint: "Versions of documentation and metadata must be the same"
}
]
}
Note that you can pass whatever number of other files, even 0 and just apply
files
and final
expressions to each file in files
property of
the rule. For example, the next configuration would not raise errors:
{
rules: [
{
files: ["foo.json"],
crossJMESPathsMatch: [
["bar", "[0].baz", 7],
]
}
]
}
{"bar": {"baz": 7}}
You can also override the Objects serialization to use for opening
other files using file/path.ext?serializer
syntax. For example, to open a
Python file line by line:
foo = True
bar = False
{
rules: [
{
files: ["file.py"], // just asserts that the file exists
crossJMESPathsMatch: [
["`null`", ["file.py?text", "[0]"], "[1]", "foo = True"],
]
}
]
}
See also
New in version 0.4.0.
ifJMESPathsMatch
Compares a set of JMESPath expressions against results.
JSON-serializes each file in the ifJMESPathsMatch
property
of the rule and executes each expression given in the first item of the
tuples passed as value for each file. If a result don’t match,
skips the rule.
Example
If inline-quotes
config of flake8 is defined to use double quotes,
black must be configured as the formatting tool in pyproject.toml
:
{
rules: [
{
files: ["pyproject.toml"],
ifJMESPathsMatch: {
"pyproject.toml": [
["tool.flakeheaven.inline_quotes", "double"],
],
},
JMESPathsMatch: [
["contains(keys(@), 'tool')", true],
["contains(keys(tool), 'black')", true],
}
}
]
}
New in version 0.1.0.
pre-commit
Plugins related to pre-commit hooks and configuration.
preCommitHookExists
Check if multiple pre-commit hooks exists in .pre-commit-config.yaml configuration, optionally defining the settings for each one.
It accepts a tuple with two elements:
A string with the URL of the repository for the
repo
key.A string with th id of a hook or an array with the configurations for each hook. This array also accepts strings with the hook ids.
Examples
{
rules: [
{
files: [".pre-commit-config.yaml"],
preCommitHookExists: [
"https://github.com/mondeja/project-config",
"project-config",
]
}
]
}
{
rules: [
{
files: [".pre-commit-config.yaml"],
preCommitHookExists: [
"https://github.com/mondeja/project-config",
"[{"id": "project-config"}]",
]
}
]
}
The rev
key is added automatically to the configuration
of the repositories if not exists in fix mode.
New in version 0.9.1.