Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Carrier pickup constants."""

MINIMUM_CHARGE_CENTS: int = 250
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Carrier pickup client."""

from providers.carrier_api.constants import MINIMUM_CHARGE_CENTS

from sqlbuild.providers import Provider

_CENTS_PER_UNIT: int = 100


class CarrierApi(Provider):
"""Client for the carrier pickup API."""

endpoint: str = "https://carrier.example.com"

def minimum_charge(self) -> float:
"""Return the minimum pickup charge in currency units."""
return MINIMUM_CHARGE_CENTS / _CENTS_PER_UNIT
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"select": ["SQBRPYTHON", "SQBRPYTHON306"],
"findings": [
["SQBRPYTHON101", "hooks/python/alert.py", 4, 11],
["SQBRPYTHON015", "providers/carrier_api/provider.py", 3, 8],
["SQBRPYTHON014", "providers/carrier_api/provider.py", 8, 5],
["SQBRPYTHON013", "providers/stock_feed.py", 1, 1],
["SQBRPYTHON613", "python/inventory/__init__.py", 1, 1],
["SQBRPYTHON002", "python/inventory/common/__init__.py", 1, 1],
["SQBRPYTHON306", "python/inventory/common/stock.py", 4, 1],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Carrier pickup client."""

import python.report

from sqlbuild.providers import Provider


def pickup_label(sku: str) -> str:
"""Return the pickup label of one product."""
return f"{sku} {python.report.__name__}"


class CarrierApi(Provider):
"""Client for the carrier pickup API."""

endpoint: str = "https://carrier.example.com"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""A provider module directly under providers/."""
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ fn union_names(annotation: &Expr) -> Vec<String> {
}

/// The last name of a name, an attribute chain, or the subscripted base of either.
fn last_name(expression: &Expr) -> Option<String> {
pub(crate) fn last_name(expression: &Expr) -> Option<String> {
match expression {
Expr::Name(name) => Some(name.id.to_string()),
Expr::Attribute(attribute) => Some(attribute.attr.to_string()),
Expand Down
1 change: 1 addition & 0 deletions crates/sqlbuild-pysource/src/node_units/_helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub(crate) mod functions;
pub(crate) mod imports;
pub(crate) mod positions;
pub(crate) mod targets;
49 changes: 49 additions & 0 deletions crates/sqlbuild-pysource/src/node_units/_helpers/targets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//! The names assignments bind and the dotted names of class bases.

use ruff_python_ast::Expr;

/// The names an assignment's targets bind; empty when any target is an attribute or subscript.
pub(crate) fn assignment_names(targets: &[Expr]) -> Vec<String> {
elements_names(targets).unwrap_or_default()
}

/// Each class base as dotted text, such as `paged.PagedApi`; empty for any other expression.
pub(crate) fn class_bases(bases: &[Expr]) -> Vec<String> {
bases.iter().map(base_name).collect()
}

/// One class base as dotted text, or empty.
fn base_name(base: &Expr) -> String {
dotted_name(base).unwrap_or_default()
}

/// The names one target binds through tuples, lists and starred targets; `None` for an attribute or subscript.
fn target_names(target: &Expr) -> Option<Vec<String>> {
match target {
Expr::Name(name) => Some(vec![name.id.to_string()]),
Expr::Tuple(tuple) => elements_names(&tuple.elts),
Expr::List(list) => elements_names(&list.elts),
Expr::Starred(starred) => target_names(&starred.value),
_ => None,
}
}

/// The names several targets bind; `None` when any of them binds no plain name.
fn elements_names(elements: &[Expr]) -> Option<Vec<String>> {
let names: Vec<Vec<String>> = elements.iter().map(target_names).collect::<Option<_>>()?;
Some(names.concat())
}

/// A name or attribute chain as dotted text, seen through a subscript such as `Generic[T]`.
fn dotted_name(expression: &Expr) -> Option<String> {
match expression {
Expr::Name(name) => Some(name.id.to_string()),
Expr::Attribute(attribute) => Some(format!(
"{}.{}",
dotted_name(&attribute.value)?,
attribute.attr
)),
Expr::Subscript(subscript) => dotted_name(&subscript.value),
_ => None,
}
}
14 changes: 13 additions & 1 deletion crates/sqlbuild-pysource/src/node_units/main/node_unit_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use crate::cpython_tree::models::ParsedSource;
use crate::node_units::_helpers::functions::{leading_docstring, unit_function};
use crate::node_units::_helpers::imports::{is_type_checking_imports, module_imports};
use crate::node_units::_helpers::positions::{UnitText, unit_position};
use crate::node_units::models::{UnitModule, UnitStatement, UnitStatementKind};
use crate::node_units::_helpers::targets::{assignment_names, class_bases};
use crate::node_units::models::{UnitClass, UnitModule, UnitStatement, UnitStatementKind};

/// The top-level statements and imports of `contents`, or `None` when it does not parse.
#[must_use]
Expand All @@ -32,6 +33,17 @@ pub fn node_unit_module(contents: &str, python_version: (u8, u8)) -> Option<Unit
Stmt::FunctionDef(function) => {
UnitStatementKind::Function(unit_function(function, &source))
}
Stmt::ClassDef(class) => UnitStatementKind::Class(UnitClass {
name: class.name.to_string(),
position: unit_position(&source, class.name.range().start()),
bases: class_bases(class.bases()),
}),
Stmt::Assign(assign) => {
UnitStatementKind::Assignment(assignment_names(&assign.targets))
}
Stmt::AnnAssign(assign) => UnitStatementKind::Assignment(assignment_names(
std::slice::from_ref(&assign.target),
)),
_ => UnitStatementKind::Other,
},
position: unit_position(&source, statement.range().start()),
Expand Down
15 changes: 14 additions & 1 deletion crates/sqlbuild-pysource/src/node_units/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,23 @@ pub enum UnitStatementKind {
/// An import, or an `if TYPE_CHECKING:` block that only imports.
Import,
Function(UnitFunction),
/// Any other statement, such as a class, an assignment or a loop.
Class(UnitClass),
/// An assignment, annotated or not, by the names it binds; empty when any target is an attribute or subscript.
Assignment(Vec<String>),
/// Any other statement, such as a loop or a call.
Other,
}

/// A class and its bases.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnitClass {
pub name: String,
/// Where the class's name is written.
pub position: UnitPosition,
/// Each base as dotted text, such as `Provider` or `paged.PagedApi`; empty when it is another expression.
pub bases: Vec<String>,
}

/// One imported name of an `import` or `from ... import` at module level.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnitImport {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,22 @@ const FACTORY_MODULE: &str = "@factory\ndef tables():\n for table in TABLES:\
fn given_module_when_reading_then_each_top_level_statement_has_its_kind() {
let test_cases = [
StatementKindsTestCase {
description: "a CRLF node module with a docstring, imports, a value and a node",
description: "a CRLF node module with a docstring, imports, an assignment and a node",
contents: CRLF_NODE_MODULE,
expected_kinds: Some(vec![
"Docstring",
"Import",
"Import",
"Import",
"Other",
"Assignment",
"Function",
]),
},
StatementKindsTestCase {
description: "a class and a loop",
contents: "class Store(Provider):\n pass\n\n\nfor row in rows:\n pass\n",
expected_kinds: Some(vec!["Class", "Other"]),
},
StatementKindsTestCase {
description: "a module that does not parse",
contents: "def broken(:\n",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ const REVIEWED_SCOPES: &[(&str, RuleScope)] = &[
("SQBRPYTHON010", RuleScope::Python),
("SQBRPYTHON011", RuleScope::Python),
("SQBRPYTHON012", RuleScope::Python),
("SQBRPYTHON013", RuleScope::Python),
("SQBRPYTHON014", RuleScope::Python),
("SQBRPYTHON015", RuleScope::Python),
("SQBRPYTHON101", RuleScope::Python),
("SQBRPYTHON102", RuleScope::Python),
("SQBRPYTHON103", RuleScope::Python),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ pub(crate) mod layout;
pub(crate) mod modules;
pub(crate) mod nodes;
pub(crate) mod project;
pub(crate) mod providers;
12 changes: 9 additions & 3 deletions crates/sqlbuild-rules/src/python_rules/_helpers/units/modules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ fn node_module_faults(file: &UnitFile<'_>) -> Vec<Fault> {
function.position,
),
},
UnitStatementKind::Other => (OTHER_STATEMENT.to_owned(), statement.position),
UnitStatementKind::Class(_)
| UnitStatementKind::Assignment(_)
| UnitStatementKind::Other => (OTHER_STATEMENT.to_owned(), statement.position),
};
faults.extend(unit_fault(
NODE_MODULE_SHAPE_CODE,
Expand Down Expand Up @@ -95,7 +97,9 @@ fn main_module_faults(file: &UnitFile<'_>, project: &UnitProject<'_>) -> Vec<Fau
format!("function '{}' is not main()", function.name),
function.position,
),
UnitStatementKind::Other => (OTHER_STATEMENT.to_owned(), statement.position),
UnitStatementKind::Class(_)
| UnitStatementKind::Assignment(_)
| UnitStatementKind::Other => (OTHER_STATEMENT.to_owned(), statement.position),
};
faults.extend(unit_fault(
MAIN_MODULE_SHAPE_CODE,
Expand Down Expand Up @@ -208,7 +212,9 @@ fn checks_module_faults(file: &UnitFile<'_>, project: &UnitProject<'_>) -> Vec<F
function.position,
)],
},
UnitStatementKind::Other => {
UnitStatementKind::Class(_)
| UnitStatementKind::Assignment(_)
| UnitStatementKind::Other => {
vec![(OTHER_STATEMENT.to_owned(), statement.position)]
}
};
Expand Down
18 changes: 14 additions & 4 deletions crates/sqlbuild-rules/src/python_rules/_helpers/units/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ fn nearest_unit(parts: &[&str], units: &BTreeSet<String>) -> Option<String> {
}

/// The dotted module of a Python path and of every package above it.
fn dotted_names(path: &str) -> Vec<String> {
pub(crate) fn dotted_names(path: &str) -> Vec<String> {
let stem: &str = path.strip_suffix(".py").unwrap_or(path);
let parts: Vec<&str> = stem
.split('/')
Expand Down Expand Up @@ -207,11 +207,21 @@ pub(crate) fn imported_module(
import: &UnitImport,
file: &UnitFile<'_>,
project: &UnitProject<'_>,
) -> Option<String> {
let mut folder: Vec<&str> = vec![PACKAGE_ROOT];
folder.extend(&file.parts[..file.parts.len() - 1]);
absolute_module(import, &folder, &project.modules)
}

/// The absolute dotted module `import` reads from a file in the project folder `folder`, given every project module.
pub(crate) fn absolute_module(
import: &UnitImport,
folder: &[&str],
modules: &BTreeSet<String>,
) -> Option<String> {
let mut base: Vec<&str> = Vec::new();
if import.level > 0 {
base.push(PACKAGE_ROOT);
base.extend(&file.parts[..file.parts.len() - 1]);
base.extend(folder);
for _ in 1..import.level {
base.pop()?;
}
Expand All @@ -222,7 +232,7 @@ pub(crate) fn imported_module(
return Some(module);
};
let submodule: String = format!("{module}.{name}");
Some(if project.modules.contains(&submodule) {
Some(if modules.contains(&submodule) {
submodule
} else {
module
Expand Down
Loading
Loading