Skip to content

Commit

Permalink
Merge feat/tags into main
Browse files Browse the repository at this point in the history
  • Loading branch information
tfeldmann committed Mar 31, 2022
2 parents 4406dca + 7bb374c commit 0189b88
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## WIP

- Tag support (#199) to run subsets of rules in your config.

## v2.1.2 (2022-02-13)

- Hotfix for `filecontent` filter.
Expand Down
28 changes: 28 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,34 @@ Optionally you can specify the working directory like this:
organize sim [FILE] --working-dir=~/Documents
```

## Running specific rules of your config

You can tag your rules like this:

```yml
rules:
- name: My first rule
actions:
- echo: "Hello world"
tags:
- debug
- fast
```
Then use the command line options `--tags` and `--skip-tags` so select the rules you
want to run. The options take a comma-separated list of tags:

```
organize sim --tags=debug,foo --skip-tags=slow
```

Special tags:

- Rules tagged with the special tag `always` will always run
(except if `--skip-tags=always` is specified)
- Rules tagged with the special tag `never` will never run
(except if ' `--tags=never` is specified)

## Environment variables

- `ORGANIZE_CONFIG` - The path to the default config file.
Expand Down
2 changes: 2 additions & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ rules:
filter_mode: ...
filters: ...
actions: ...
tags: ...

# Another rule
- name: ...
Expand All @@ -51,6 +52,7 @@ The rule options in detail:
- **filter_mode** (`str`): `"all"`, `"any"` or `"none"` of the filters must apply _(Default: `"all"`)_
- **filters** (`list`): A list of [filters](filters.md) _(Default: `[]`)_
- **actions** (`list`): A list of [actions](actions.md)
- **tags** (`list`): A list of [tags](configuration.md#running-specific-rules-of-your-config)

## Targeting directories

Expand Down
53 changes: 46 additions & 7 deletions organize/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
"""
import os
import sys
from pathlib import Path
from typing import Tuple

import click
from fs import appfs
from pathlib import Path

from . import console
from .__version__ import __version__
Expand Down Expand Up @@ -57,6 +58,15 @@ def list_commands(self, ctx):
return self.commands.keys()


class TagType(click.ParamType):
name = "tag"

def convert(self, value, param, ctx):
if not value:
return tuple()
return tuple(tag.strip() for tag in value.split(","))


CLI_CONFIG = click.argument(
"config",
required=False,
Expand All @@ -76,9 +86,17 @@ def list_commands(self, ctx):
hidden=True,
type=click.Path(exists=True, dir_okay=False, path_type=Path),
)
CLI_TAGS = click.option("--tags", type=TagType(), default="")
CLI_SKIP_TAGS = click.option("--skip-tags", type=TagType(), default="")


def run_local(config_path: Path, working_dir: Path, simulate: bool):
def run_local(
config_path: Path,
working_dir: Path,
simulate: bool,
tags: Tuple[str] = tuple(),
skip_tags: Tuple[str] = tuple(),
):
from schema import SchemaError

from . import core
Expand All @@ -87,7 +105,12 @@ def run_local(config_path: Path, working_dir: Path, simulate: bool):
console.info(config_path=config_path, working_dir=working_dir)
config = config_path.read_text()
os.chdir(working_dir)
core.run(rules=config, simulate=simulate)
core.run(
rules=config,
simulate=simulate,
tags=tags,
skip_tags=skip_tags,
)
except NeedsMigrationError as e:
console.error(e, title="Config needs migration")
console.warn(
Expand Down Expand Up @@ -122,28 +145,44 @@ def cli():
@CLI_CONFIG
@CLI_WORKING_DIR_OPTION
@CLI_CONFIG_FILE_OPTION
def run(config: Path, working_dir: Path, config_file):
@CLI_TAGS
@CLI_SKIP_TAGS
def run(config: Path, working_dir: Path, config_file, tags, skip_tags):
"""Organizes your files according to your rules."""
if config_file:
config = config_file
console.deprecated(
"The --config-file option can now be omitted. See organize --help."
)
run_local(config_path=config, working_dir=working_dir, simulate=False)
run_local(
config_path=config,
working_dir=working_dir,
simulate=False,
tags=tags,
skip_tags=skip_tags,
)


@cli.command()
@CLI_CONFIG
@CLI_WORKING_DIR_OPTION
@CLI_CONFIG_FILE_OPTION
def sim(config: Path, working_dir: Path, config_file):
@CLI_TAGS
@CLI_SKIP_TAGS
def sim(config: Path, working_dir: Path, config_file, tags, skip_tags):
"""Simulates a run (does not touch your files)."""
if config_file:
config = config_file
console.deprecated(
"The --config-file option can now be omitted. See organize --help."
)
run_local(config_path=config, working_dir=working_dir, simulate=True)
run_local(
config_path=config,
working_dir=working_dir,
simulate=True,
tags=tags,
skip_tags=skip_tags,
)


@cli.command()
Expand Down
1 change: 1 addition & 0 deletions organize/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
Optional("filter_mode", description="The filter mode."): Or(
"all", "any", "none", error="Invalid filter mode"
),
Optional("tags"): Or(str, [str]),
Optional(
"targets",
description="Whether the rule should apply to directories or folders.",
Expand Down
43 changes: 40 additions & 3 deletions organize/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,14 +239,45 @@ def action_pipeline(actions: Iterable[Action], args: dict, simulate: bool) -> bo
return True


def run_rules(rules: dict, simulate: bool = True):
def should_execute(rule_tags, tags, skip_tags):
if not rule_tags:
rule_tags = set()
if not tags:
tags = set()
if not skip_tags:
skip_tags = set()

if "always" in rule_tags and "always" not in skip_tags:
return True
if "never" in rule_tags and "never" not in tags:
return False
if not tags and not skip_tags:
return True
if not rule_tags and tags:
return False
should_run = any(tag in tags for tag in rule_tags) or not tags or not rule_tags
should_skip = any(tag in skip_tags for tag in rule_tags)
return should_run and not should_skip


def run_rules(rules: dict, tags, skip_tags, simulate: bool = True):
count = Counter(done=0, fail=0) # type: Counter

if simulate:
console.simulation_banner()

console.spinner(simulate=simulate)
for rule_nr, rule in enumerate(rules["rules"], start=1):
rule_tags = rule.get("tags")
if isinstance(rule_tags, str):
rule_tags = [tag.strip() for tag in rule_tags.split(",")]
should_run = should_execute(
rule_tags=rule_tags,
tags=tags,
skip_tags=skip_tags,
)
if not should_run:
continue
target = rule.get("targets", "files")
console.rule(rule.get("name", "Rule %s" % rule_nr))
filter_mode = rule.get("filter_mode", "all")
Expand Down Expand Up @@ -304,7 +335,13 @@ def run_rules(rules: dict, simulate: bool = True):
return count


def run(rules: Union[str, dict], simulate: bool, validate=True):
def run(
rules: Union[str, dict],
simulate: bool,
validate=True,
tags=None,
skip_tags=None,
):
# load and validate
if isinstance(rules, str):
rules = config.load_from_string(rules)
Expand All @@ -322,7 +359,7 @@ def run(rules: Union[str, dict], simulate: bool, validate=True):
console.warn(msg)

# run
count = run_rules(rules=rules, simulate=simulate)
count = run_rules(rules=rules, tags=tags, skip_tags=skip_tags, simulate=simulate)
console.summary(count)

if count["fail"]:
Expand Down
5 changes: 1 addition & 4 deletions organize/filters/created.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,4 @@ def fallback_method(self, fs, fs_path):
pass

def __str__(self):
return "[Created] All files / folders %s than %s" % (
self._mode,
self.timedelta,
)
return "<Created>"
43 changes: 43 additions & 0 deletions tests/core/test_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from organize.core import should_execute


def test_no_tags_given():
assert should_execute(rule_tags=None, tags=None, skip_tags=None)
assert should_execute(rule_tags=["tag"], tags=None, skip_tags=None)
assert should_execute(rule_tags=["tag", "tag2"], tags=None, skip_tags=None)


def test_run_tagged():
assert not should_execute(rule_tags=None, tags=["tag"], skip_tags=None)
assert should_execute(rule_tags=["tag"], tags=["tag"], skip_tags=None)
assert should_execute(rule_tags=["tag", "tag2"], tags=["tag"], skip_tags=None)
assert not should_execute(rule_tags=["foo", "bar"], tags=["tag"], skip_tags=None)
assert not should_execute(rule_tags=["taggity"], tags=["tag"], skip_tags=None)


def test_skip():
assert should_execute(rule_tags=None, tags=None, skip_tags=["tag"])
assert should_execute(rule_tags=["tag"], tags=None, skip_tags=["asd"])
assert not should_execute(rule_tags=["tag"], tags=None, skip_tags=["tag"])
assert not should_execute(rule_tags=["tag", "tag2"], tags=None, skip_tags=["tag"])


def test_combination():
assert not should_execute(rule_tags=None, tags=["tag"], skip_tags=["foo"])
assert not should_execute(rule_tags=["foo", "tag"], tags=["tag"], skip_tags=["foo"])


def test_always():
assert should_execute(rule_tags=["always"], tags=["debug", "test"], skip_tags=None)
assert should_execute(rule_tags=["always", "tag"], tags=None, skip_tags=["tag"])
# skip only if specifically requested
assert not should_execute(
rule_tags=["always", "tag"], tags=None, skip_tags=["always"]
)


def test_never():
assert not should_execute(rule_tags=["never"], tags=None, skip_tags=None)
assert not should_execute(rule_tags=["never", "tag"], tags=["tag"], skip_tags=None)
# run only if specifically requested
assert should_execute(rule_tags=["never", "tag"], tags=["never"], skip_tags=None)

0 comments on commit 0189b88

Please sign in to comment.