From 5a30dbd6d006fca72ae42d00f72499f2660c8415 Mon Sep 17 00:00:00 2001 From: stu Date: Fri, 18 Oct 2024 09:36:49 +0800 Subject: [PATCH] use ruff for pre-commit and run it across all files --- .pre-commit-config.yaml | 29 +++++------------------------ .ruff.toml | 12 ++++++++++++ client_code/atomic/__init__.py | 1 + client_code/atomic/atoms.py | 12 +++++++----- client_code/atomic/contexts.py | 18 ++++++++++++------ client_code/atomic/decorators.py | 14 +++++++++----- client_code/atomic/helpers.py | 15 ++++++++++----- client_code/atomic/registrar.py | 3 ++- client_code/atomic/rendering.py | 6 ++++-- client_code/atomic/subscribers.py | 3 ++- client_code/cluegen.py | 10 ++++++---- client_code/fido.py | 6 ++++-- client_code/kompot/__init__.py | 2 ++ client_code/kompot/_register.py | 1 - client_code/kompot/_rpc.py | 1 - client_code/non_blocking.py | 13 +++++++++---- client_code/web_worker.py | 2 +- server_code/fido_webauthn.py | 1 - server_code/web_worker_endpoint.py | 5 ++--- tests/test_atomic.py | 13 ++++++------- tests/test_pedantic.py | 1 - 21 files changed, 94 insertions(+), 74 deletions(-) create mode 100644 .ruff.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index acb2b93..c7cd810 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,33 +5,14 @@ repos: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/psf/black - rev: 23.3.0 # Replace by any tag/version: https://github.com/psf/black/tags - hooks: - - id: black - args: # arguments to configure black - - --line-length=88 - language_version: python3 # Should be a command that runs python3.6+ - - - repo: https://github.com/pycqa/isort - rev: 5.12.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.4 hooks: - - id: isort - name: isort (python) + - id: ruff + args: [--fix] + - id: ruff-format - # flake8 - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 - hooks: - - id: flake8 - args: # arguments to configure flake8 - # making isort line length compatible with black - - "--max-line-length=88" - # these are errors that will be ignored by flake8 - # check out their meaning here - # https://flake8.pycqa.org/en/latest/user/error-codes.html - - "--ignore=E203,E266,E501,W503,F403,F401,E402,W605,E302" - repo: https://github.com/Lucas-C/pre-commit-hooks rev: v1.5.1 hooks: diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..dca97bb --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,12 @@ +src = ["client_code", "server_code"] + +[lint] +select = ["E", "F", "I001"] + +[lint.isort] +known-third-party = ["anvil"] + +[lint.pycodestyle] +# E501 reports lines that exceed the length of 100. +# this only happens when we can't wrap the line with ruff-format +max-line-length = 100 diff --git a/client_code/atomic/__init__.py b/client_code/atomic/__init__.py index 752221f..0cbaa2c 100644 --- a/client_code/atomic/__init__.py +++ b/client_code/atomic/__init__.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: MIT # Copyright (c) 2021 anvilistas +# ruff: noqa: F401 from .atoms import DictAtom, ListAtom, atom, portable_atom from .contexts import ignore_updates from .decorators import ( diff --git a/client_code/atomic/atoms.py b/client_code/atomic/atoms.py index 56a75fe..0b8d52a 100644 --- a/client_code/atomic/atoms.py +++ b/client_code/atomic/atoms.py @@ -19,7 +19,8 @@ class BaseAction( namedtuple("_BaseAction", ["action", "atom", "prop", "value"], defaults=[None]) ): - """We use this as something we can pass to the action queue and might be consumed by a decorated subscriber""" + """We use this as something we can pass to the action queue + and might be consumed by a decorated subscriber""" def __str__(self): action, atom, prop, val = self @@ -28,9 +29,9 @@ def __str__(self): def as_atom(atom, prop, val): - if type(val) is dict: + if type(val) is dict: # noqa: E721 return DictAtom(val) - elif type(val) is list: + elif type(val) is list: # noqa: E721 return ListAtom(atom, prop, val) else: return val @@ -106,7 +107,7 @@ def portable_atom(_cls, name=None): """decorator to for atoms that you also want to be portable classes""" if IS_SERVER_SIDE: return portable_class(_cls, name) - elif name is None and type(_cls) is str: + elif name is None and isinstance(_cls, str): name = _cls return lambda _cls: portable_atom(_cls, name) @@ -237,7 +238,8 @@ def fn(self, *args, **kws): class ListAtom(list): """ - Any time the list is mutated we request a render from the parent atom at the property this list belongs to + Any time the list is mutated we request a render from the parent atom + at the property this list belongs to """ __slots__ = ["_as_atom", "_request_render", "_register"] diff --git a/client_code/atomic/contexts.py b/client_code/atomic/contexts.py index 9deddc7..c540c85 100644 --- a/client_code/atomic/contexts.py +++ b/client_code/atomic/contexts.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: MIT # Copyright (c) 2021 anvilistas -from functools import partial from .constants import ACTION, IGNORE, REACTION, RENDER, SELECTOR from .rendering import active, call_queued, log, queued @@ -78,8 +77,11 @@ class ActionContext(Context): mode = ACTION def adder(self): - msg = "Cannot update an Atom or call an action from inside a selector or render method \ - - use `with ignore_updates:` if you really need to update an Atom attribute" + msg = ( + "Cannot update an Atom or call an action from inside a selector or" + "render method - use `with ignore_updates:`" + " if you really need to update an Atom attribute" + ) self.add_active((SELECTOR, RENDER, REACTION), msg) queued[ACTION] += (self.context,) @@ -112,12 +114,16 @@ def adder(self): class ReactionContext(Context): # note the ReactionContext only applies to the depends_on_fn call - # There should only be attribute access and selector method calls within this context + # There should only be attribute access + # and selector method calls within this context mode = REACTION def adder(self): - msg = "The reaction depends_on_fn should only access atom attributes.\ - Calling a depends_on_fn from inside a selector, render or other depends_on_fn is invalid" + msg = ( + "The reaction depends_on_fn should only access atom attributes." + "Calling a depends_on_fn from inside a selector," + " render or other depends_on_fn is invalid" + ) self.add_active((SELECTOR, RENDER, REACTION), msg) popper = Context.pop_active diff --git a/client_code/atomic/decorators.py b/client_code/atomic/decorators.py index 5b7375c..af5b668 100644 --- a/client_code/atomic/decorators.py +++ b/client_code/atomic/decorators.py @@ -22,7 +22,8 @@ def _get_selector(fn, atom, prop): def selector(fn): - """decorate a method as a selector whenever it needs to do some computation based on atom attributes + """decorate a method as a selector whenever it needs to do + some computation based on atom attributes This decorate can only be used on an atom method You should never update an atom within a selector A selector can be decorated with @property @@ -99,7 +100,8 @@ def unsubscribe(f): class render: """a decorator typically used above a method in a form if used on a form render methods will only execute on the show event - if used as a top level function it can be used to update a database whenever an atom attribute changes + if used as a top level function it can be used to update a database + whenever an atom attribute changes a render should access all selectors and atom attributes that it depends on i.e. don't access some attributes within branching logic (if statements) @@ -148,12 +150,14 @@ def reaction( include_previous=False, ): """a reaction takes two arguments: depends_on_fn and then_react_fn - the depends_on_fn is used to determine the dependcies that the then_react_fn depends on + the depends_on_fn is used to determine the dependencies that the then_react_fn depends on when ever an atom attribute accessed in the depends_on_fn changes the then_react_fn is called. - If the depends_on_fn returns a value other than None the return value will be passed to the then_react_fn. + If the depends_on_fn returns a value other than None + the return value will be passed to the then_react_fn. - depends_on_fn fires immediately, but then_react_fn will only be called the next time a dependency changes. + depends_on_fn fires immediately, but then_react_fn will only be called + the next time a dependency changes. To call the then_react_fn function immediately set fire_immediately to True. Returns: a dispose function - when called stops any future reactions diff --git a/client_code/atomic/helpers.py b/client_code/atomic/helpers.py index 80ff3c7..e6148e9 100644 --- a/client_code/atomic/helpers.py +++ b/client_code/atomic/helpers.py @@ -17,12 +17,13 @@ def set_debug(is_debug=True): def writeback(component, prop, atom_or_selector, attr_or_action=None, events=()): """create a writeback between a component property and an atom attribute - or bind the property to an atom selector and call an action when the component property is changed + or bind the property to an atom selector and call an action when + the component property is changed events - should be a single event str or a list of events If no events are provided this is the equivalent of a data-binding with no writeback """ atom, attr = atom_or_selector, attr_or_action - if type(events) is str: + if isinstance(events, str): events = [events] if isinstance(atom, dict): assert attr is not None, "if a dict atom is provided the attr must be a str" @@ -58,9 +59,13 @@ def _noop(): def bind(component, prop, atom_or_selector, attr=SENTINEL): - """create a data-binding between an component property and an atom and its attribute (or a selector)""" - # we could support methods here but it's better to be explicit and call selectors inside render methods - # accessing an atom property necessarily creates a depenedency on the current render context + """ + create a data-binding between an component property + and an atom and its attribute (or a selector) + """ + # we could support methods here but it's better to be explicit + # and call selectors inside render methods + # accessing an atom property necessarily creates a dependency on the current render context # so better not to encourage accessing a selector outside of the desired render context attr = _noop if attr is SENTINEL else attr return writeback(component, prop, atom_or_selector, attr) diff --git a/client_code/atomic/registrar.py b/client_code/atomic/registrar.py index 6375e5a..a410b77 100644 --- a/client_code/atomic/registrar.py +++ b/client_code/atomic/registrar.py @@ -7,7 +7,8 @@ class AtomRegistrar: - """the registrar is responsible for registering and unregistering atom props to renderers/selectors + """the registrar is responsible for registering and un-registering + atom props to renderers/selectors as well as caching selectors associated with an atom""" def __init__(self, atom): diff --git a/client_code/atomic/rendering.py b/client_code/atomic/rendering.py index 7e91900..667a677 100644 --- a/client_code/atomic/rendering.py +++ b/client_code/atomic/rendering.py @@ -24,7 +24,8 @@ def log(fn): def register(atom, prop): """if there is an active selector or render - we asks the atom registrar to register a relationship between an atom and the attribute being accessed + we asks the atom registrar to register a relationship + between an atom and the attribute being accessed """ if active[IGNORE]: return @@ -91,7 +92,8 @@ def queue_subscribers(atom_registrar, prop, mode): def request(atom, prop): - """when an attribute of an atom is accessed we update the queues based on the subscribers registered""" + """when an attribute of an atom is accessed + we update the queues based on the subscribers registered""" if active[IGNORE]: return atom_registrar = get_registrar(atom) diff --git a/client_code/atomic/subscribers.py b/client_code/atomic/subscribers.py index f917fe2..9d9f4ab 100644 --- a/client_code/atomic/subscribers.py +++ b/client_code/atomic/subscribers.py @@ -118,7 +118,8 @@ def compute_cached(self): def __call__(self, *args, **kws): # anytime our value is requested make renders/selectors depend on our property - # we don't use atom's __getattribute__ for registration since it doesn't register methods accessed + # we don't use atom's __getattribute__ for registration + # since it doesn't register methods accessed # and we only want the registration to occur when we call the selector # this allows selectors to be used as the bind/writeback function register(self.atom, self.prop) diff --git a/client_code/cluegen.py b/client_code/cluegen.py index 39ac91e..e6243c0 100644 --- a/client_code/cluegen.py +++ b/client_code/cluegen.py @@ -71,10 +71,12 @@ def __init_subclass__(cls): def __init__(cls): clues = all_clues(cls) args = ", ".join( - f"{name}={getattr(cls,name)!r}" - if hasattr(cls, name) - and not isinstance(getattr(cls, name), MemberDescriptorType) - else name + ( + f"{name}={getattr(cls, name)!r}" + if hasattr(cls, name) + and not isinstance(getattr(cls, name), MemberDescriptorType) + else name + ) for name in clues ) body = "\n".join(f" self.{name} = {name}" for name in clues) diff --git a/client_code/fido.py b/client_code/fido.py index 556edfc..5f14e50 100644 --- a/client_code/fido.py +++ b/client_code/fido.py @@ -21,7 +21,8 @@ def verify_registration(response): def register_device(): - """a logged in user is required to register a device. The user table must have a 'fido' simple object column""" + """a logged in user is required to register a device. + The user table must have a 'fido' simple object column""" public_key = generate_registration() try: resposne = startRegistration(public_key) @@ -44,7 +45,8 @@ def verify_authentication_options(authentication_options): def login_with_fido(email: str): - """provide an email address to login with fido - this email might be stored in indexed db or local storage or similar""" + """provide an email address to login with fido + this email might be stored in indexed db or local storage or similar""" authentication_options = generate_authentication_options(email) result = verify_authentication_options(authentication_options) if result is None: diff --git a/client_code/kompot/__init__.py b/client_code/kompot/__init__.py index 2e7b8c0..8b21a9f 100644 --- a/client_code/kompot/__init__.py +++ b/client_code/kompot/__init__.py @@ -1,5 +1,7 @@ # SPDX-License-Identifier: MIT # Copyright (c) 2021 anvilistas + +# ruff: noqa: F401 from ._batcher import batch_call from ._register import register from ._rpc import call, call_async, call_s, callable diff --git a/client_code/kompot/_register.py b/client_code/kompot/_register.py index 5555a68..bd2a1a7 100644 --- a/client_code/kompot/_register.py +++ b/client_code/kompot/_register.py @@ -2,7 +2,6 @@ # Copyright (c) 2021 anvilistas from anvil.server import SerializationError - from anvil_extras.utils import import_module __version__ = "0.0.1" diff --git a/client_code/kompot/_rpc.py b/client_code/kompot/_rpc.py index 059a918..b1a8df4 100644 --- a/client_code/kompot/_rpc.py +++ b/client_code/kompot/_rpc.py @@ -4,7 +4,6 @@ from functools import wraps as _wraps import anvil.server as _server -from anvil import is_server_side from ._serialize import UNHANDLED, reconstruct, serialize diff --git a/client_code/non_blocking.py b/client_code/non_blocking.py index e895b40..39b7f62 100644 --- a/client_code/non_blocking.py +++ b/client_code/non_blocking.py @@ -163,8 +163,10 @@ def __init__(self, fn, delay=None): if not _warning: _warning = True print( - "WARNING: Interval and Timeout have been deprecated in favour of repeat() and defer() and will soon be removed." - "\nSee latest documentation: https://anvil-labs.readthedocs.io/en/latest/guides/modules/non_blocking.html" + "WARNING: Interval and Timeout have been deprecated in favour of" + " repeat() and defer() and will soon be removed." + "\nSee latest documentation:" + " https://anvil-labs.readthedocs.io/en/latest/guides/modules/non_blocking.html" ) assert callable( fn @@ -234,7 +236,10 @@ def cancel(ref): if ref is None: return if not isinstance(ref, TimerRef): - msg = "Invalid argumnet to cancel(), expected None or the return value from calling delay/defer" + msg = ( + "Invalid argumnet to cancel()," + " expected None or the return value from calling delay/defer" + ) raise TypeError(msg) return ref.cancel() @@ -346,5 +351,5 @@ def _f(v): else: assert False _v = call_async(lambda: {}).await_result() - assert type(_v) is dict + assert isinstance(_v, dict) print("PASSED") diff --git a/client_code/web_worker.py b/client_code/web_worker.py index a2661e1..d9de9e8 100644 --- a/client_code/web_worker.py +++ b/client_code/web_worker.py @@ -48,7 +48,7 @@ def portable_class(cls,name=None): (${j})(); }) `;function L(){let e={resolve:()=>{},reject:()=>{}};return e.promise=new Promise((t,r)=>{e.resolve=t,e.reject=r}),e}var{misceval:{callsimArray:E,buildClass:H},ffi:{toPy:K},builtin:{RuntimeError:T,str:I,print:N}}=Sk,S=H({__name__:"anvil_labs.web_worker"},()=>{},"WorkerTaskKilled",[T]);function z(e,t,r){let n;if(e==="WorkerTaskKilled")return n=E(S,t),n.traceback=r,n;try{let a=Sk.builtin[e];n=E(a,t.map(s=>K(s))),n.traceback=r!=null?r:[]}catch{let a;try{a=new window[e](...t)}catch{a=new Error(...t)}n=new Sk.builtin.ExternalError(a)}return n}function F(e){let t={};e.currentTask=null,e.stateHandler=()=>{},e.launchTask=(r,n,a)=>{let s=Math.random().toString(36).substring(6);return e.currentTask={fn:r,id:s},e.postMessage({type:"CALL",id:s,fn:r,args:n,kws:a}),t[s]=L(),[s,r,new Promise(o=>o(t[s].promise))]},e.onmessage=async({data:r})=>{var n;switch(r.type){case"OUT":{let{id:a,fn:s}=(n=e.currentTask)!=null?n:{};N([`:`,r.message],["end",I.$empty]);break}case"STATE":{e.stateHandler({...r.state});break}case"RESPONSE":{let{id:a,value:s,errorType:o,errorArgs:i,errorTb:l}=r,c=t[r.id];c?o?(console.debug(`RPC error response ${a}:`,o,i),c.reject(z(o,i,l))):(console.debug(`RPC response ${a}:`,s),c.resolve(s)):console.warn(`Got worker response for invalid call ${r.id}`,r),delete t[r.id],e.currentTask=null;return}case"IMPORT":{let{id:a,filename:s}=r,o=null;try{o=Sk.read(s),typeof o!="string"&&(o=await Sk.misceval.asyncToPromise(()=>o))}catch{if(s.startsWith("app/")){let[i,l,c]=s.split("/");c==="__init__.py"&&l!=="anvil"&&(o="pass")}else o=null}e.postMessage({type:"MODULE",id:a,content:o})}}}}var b=class{constructor(t,r,n,a){this._id=t;this._name=r;this._result=n;this._target=a;_(this,"_handledResult");_(this,"_startTime");_(this,"_state",{});_(this,"_rv",null);_(this,"_err",null);_(this,"_complete",!1);_(this,"_status",null);_(this,"_stateHandler",()=>{});this._startTime=Date.now(),this._handledResult=n,this._target.stateHandler=this._updateState.bind(this),this._result.then(s=>{this._complete=!0,this._status="completed",this._rv=s},s=>{this._complete=!0,s instanceof S?(this._status="killed",this._err=new T("'"+this._name+"' worker task was killed")):(this._status="failed",this._err=s)})}_updateState(t){this._state=t,this._stateHandler(t)}await_result(){return this._result}on_result(t,r){this._handledResult=this._result.then(t),r&&(this._handledResult=this._handledResult.catch(r))}on_error(t){this._handledResult=this._handledResult.catch(t)}on_state_change(t){this._stateHandler=t}get_state(){return this._state}get_id(){return this._id}get_task_name(){return this._name}get_termination_status(){return this._status}get_return_value(){if(this._err)throw this._err;return this._rv}get_error(){if(this._err!==null)throw this._err}get_start_time(){return this._startTime}is_completed(){if(this._err)throw this._err;return this._complete}is_running(){return!this._complete}kill(){this._complete||this._target.postMessage({type:"KILL",id:this._id})}},v=class{constructor(t){_(this,"target");t.startsWith(window.anvilAppMainPackage)||(t=window.anvilAppMainPackage+"."+t);let r=M.replace("{$filename$}",JSON.stringify(t)).replace("self.anvilAppOrigin",JSON.stringify(window.anvilAppOrigin)),n=new Blob([r],{type:"text/javascript"});this.target=new Worker(URL.createObjectURL(n)),F(this.target)}launch_task(t,r,n){if(this.target.currentTask!==null)throw new T("BackgroundWorker already has an active task");let[s,o,i]=this.target.launchTask(t,r,n);return new b(s,o,i,this.target)}};var C;(C=window.anvilLabs)!=null||(window.anvilLabs={});window.anvilLabs.Worker=v;window.anvilLabs.WorkerTaskKilled=S;})(); -""" +""" # noqa: E501 _blob = _W.Blob([_script], {type: "text/javascript"}) _src = _W.URL.createObjectURL(_blob) diff --git a/server_code/fido_webauthn.py b/server_code/fido_webauthn.py index 10de216..2ad89aa 100644 --- a/server_code/fido_webauthn.py +++ b/server_code/fido_webauthn.py @@ -4,7 +4,6 @@ import base64 import json import urllib.parse -from base64 import b64decode from functools import wraps import anvil.server diff --git a/server_code/web_worker_endpoint.py b/server_code/web_worker_endpoint.py index 9c29821..0526da0 100644 --- a/server_code/web_worker_endpoint.py +++ b/server_code/web_worker_endpoint.py @@ -1,12 +1,11 @@ # SPDX-License-Identifier: MIT # Copyright (c) 2021 anvilistas -__version__ = "0.0.1" - import anvil.server - from anvil_labs import kompot +__version__ = "0.0.1" + @anvil.server.http_endpoint("/anvil_labs_private_call") def anvil_labs_private_call(name): diff --git a/tests/test_atomic.py b/tests/test_atomic.py index 9d980ed..282b6de 100644 --- a/tests/test_atomic.py +++ b/tests/test_atomic.py @@ -1,12 +1,7 @@ # SPDX-License-Identifier: MIT # Copyright (c) 2021 anvilistas import anvil - import pytest - -is_server_side = anvil.is_server_side -anvil.is_server_side = lambda: False # so that atomic thinks we're client side - from client_code.atomic import ( action, atom, @@ -22,6 +17,10 @@ writeback, ) +is_server_side = anvil.is_server_side +anvil.is_server_side = lambda: False # so that atomic thinks we're client side + + anvil.is_server_side = is_server_side __version__ = "0.0.1" @@ -191,8 +190,8 @@ def test_todos(): assert isinstance(todos_atom.todos, list) assert isinstance(todos_atom.todos[0], dict) - assert type(todos_atom.todos) is not list - assert type(todos_atom.todos[0]) is not dict + assert type(todos_atom.todos) is not list # noqa: E721 + assert type(todos_atom.todos[0]) is not dict # noqa: E721 # test dict view forces a render - but the action isn't caught by the subscriber todos_atom.todos[0]["completed"] = True diff --git a/tests/test_pedantic.py b/tests/test_pedantic.py index ea47a03..58be2a8 100644 --- a/tests/test_pedantic.py +++ b/tests/test_pedantic.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: MIT # Copyright (c) 2021 anvilistas import pytest - from client_code.pedantic import InList, validate __version__ = "0.0.1"