Skip to content

Commit 2c7f02d

Browse files
authored
Merge pull request #524 from sanders41/extra-package-version
Add option to specify version for extra dependencies
2 parents d0e6259 + caa3df1 commit 2c7f02d

13 files changed

+275
-4
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,13 @@ python-project create -s
201201
- Extra Python Dependencies
202202

203203
These are extra packages you want to include in the project that are not provided by default.
204-
For example specifying `fastapi, meilisearch-python-sdk` here will these two packages in the
204+
For example specifying `fastapi, camel-converter` here will these two packages in the
205205
dependencies. If the project is an application the version will be pinned to the latest release
206206
of the packages, and if it is a library the latest release will be used for the minimum version.
207+
If you would like to specify a specific version instead you can by the version with an `@`.
208+
For example `[email protected], [email protected]`. When creating an appliction FastAPI will
209+
be pinned to `0.115.0` and Camel Converter will be pinned to `4.0.0`, or if creating a library
210+
these versions will be used for the minimum version.
207211

208212
After running the generator a new directory will be created with the name you used for the
209213
`Project Slug`. Change to this directory then install the python packages and pre-commit hooks.

src/package_version.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,16 @@ pub struct ExtraPythonPackageVersion {
145145

146146
impl ExtraPythonPackageVersion {
147147
pub fn new(package: String) -> Result<Self> {
148-
let version = get_latest_python_version(&package)?;
149-
150-
Ok(ExtraPythonPackageVersion { package, version })
148+
if let Some(p) = package.split_once("@") {
149+
Ok(ExtraPythonPackageVersion {
150+
package: p.0.to_string(),
151+
version: p.1.to_string(),
152+
})
153+
} else {
154+
let version = get_latest_python_version(&package)?;
155+
156+
Ok(ExtraPythonPackageVersion { package, version })
157+
}
151158
}
152159
}
153160

src/project_generator.rs

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1237,6 +1237,48 @@ mod tests {
12371237
assert_yaml_snapshot!(content);
12381238
}
12391239

1240+
#[test]
1241+
fn test_save_poetry_pyproject_toml_application_with_python_extras() {
1242+
let mut project_info = project_info_dummy();
1243+
project_info.project_manager = ProjectManager::Poetry;
1244+
project_info.is_application = true;
1245+
project_info.extra_python_packages = Some(vec![
1246+
"[email protected]".to_string(),
1247+
"[email protected]".to_string(),
1248+
]);
1249+
let base = project_info.base_dir();
1250+
create_dir_all(&base).unwrap();
1251+
let expected_file = base.join("pyproject.toml");
1252+
save_pyproject_toml_file(&project_info).unwrap();
1253+
1254+
assert!(expected_file.is_file());
1255+
1256+
let content = std::fs::read_to_string(expected_file).unwrap();
1257+
1258+
assert_yaml_snapshot!(content);
1259+
}
1260+
1261+
#[test]
1262+
fn test_save_poetry_pyproject_toml_lib_with_python_extras() {
1263+
let mut project_info = project_info_dummy();
1264+
project_info.project_manager = ProjectManager::Poetry;
1265+
project_info.is_application = false;
1266+
project_info.extra_python_packages = Some(vec![
1267+
"[email protected]".to_string(),
1268+
"[email protected]".to_string(),
1269+
]);
1270+
let base = project_info.base_dir();
1271+
create_dir_all(&base).unwrap();
1272+
let expected_file = base.join("pyproject.toml");
1273+
save_pyproject_toml_file(&project_info).unwrap();
1274+
1275+
assert!(expected_file.is_file());
1276+
1277+
let content = std::fs::read_to_string(expected_file).unwrap();
1278+
1279+
assert_yaml_snapshot!(content);
1280+
}
1281+
12401282
#[test]
12411283
fn test_save_poetry_pyproject_toml_file_apache_application() {
12421284
let mut project_info = project_info_dummy();
@@ -1309,6 +1351,48 @@ mod tests {
13091351
assert_yaml_snapshot!(content);
13101352
}
13111353

1354+
#[test]
1355+
fn test_save_pyproject_toml_file_pyo3_application_with_python_extras() {
1356+
let mut project_info = project_info_dummy();
1357+
project_info.project_manager = ProjectManager::Maturin;
1358+
project_info.is_application = true;
1359+
project_info.extra_python_packages = Some(vec![
1360+
"[email protected]".to_string(),
1361+
"[email protected]".to_string(),
1362+
]);
1363+
let base = project_info.base_dir();
1364+
create_dir_all(&base).unwrap();
1365+
let expected_file = base.join("pyproject.toml");
1366+
save_pyproject_toml_file(&project_info).unwrap();
1367+
1368+
assert!(expected_file.is_file());
1369+
1370+
let content = std::fs::read_to_string(expected_file).unwrap();
1371+
1372+
assert_yaml_snapshot!(content);
1373+
}
1374+
1375+
#[test]
1376+
fn test_save_pyproject_toml_file_pyo3_lib_with_python_extras() {
1377+
let mut project_info = project_info_dummy();
1378+
project_info.project_manager = ProjectManager::Maturin;
1379+
project_info.is_application = false;
1380+
project_info.extra_python_packages = Some(vec![
1381+
"[email protected]".to_string(),
1382+
"[email protected]".to_string(),
1383+
]);
1384+
let base = project_info.base_dir();
1385+
create_dir_all(&base).unwrap();
1386+
let expected_file = base.join("pyproject.toml");
1387+
save_pyproject_toml_file(&project_info).unwrap();
1388+
1389+
assert!(expected_file.is_file());
1390+
1391+
let content = std::fs::read_to_string(expected_file).unwrap();
1392+
1393+
assert_yaml_snapshot!(content);
1394+
}
1395+
13121396
#[test]
13131397
fn test_save_pyproject_toml_file_apache_pyo3() {
13141398
let mut project_info = project_info_dummy();
@@ -1363,6 +1447,48 @@ mod tests {
13631447
assert_yaml_snapshot!(content);
13641448
}
13651449

1450+
#[test]
1451+
fn test_save_pyproject_toml_file_setuptools_application_with_python_extras() {
1452+
let mut project_info = project_info_dummy();
1453+
project_info.project_manager = ProjectManager::Setuptools;
1454+
project_info.is_application = true;
1455+
project_info.extra_python_packages = Some(vec![
1456+
"[email protected]".to_string(),
1457+
"[email protected]".to_string(),
1458+
]);
1459+
let base = project_info.base_dir();
1460+
create_dir_all(&base).unwrap();
1461+
let expected_file = base.join("pyproject.toml");
1462+
save_pyproject_toml_file(&project_info).unwrap();
1463+
1464+
assert!(expected_file.is_file());
1465+
1466+
let content = std::fs::read_to_string(expected_file).unwrap();
1467+
1468+
assert_yaml_snapshot!(content);
1469+
}
1470+
1471+
#[test]
1472+
fn test_save_pyproject_toml_file_setuptools_lib_with_python_extras() {
1473+
let mut project_info = project_info_dummy();
1474+
project_info.project_manager = ProjectManager::Setuptools;
1475+
project_info.is_application = false;
1476+
project_info.extra_python_packages = Some(vec![
1477+
"[email protected]".to_string(),
1478+
"[email protected]".to_string(),
1479+
]);
1480+
let base = project_info.base_dir();
1481+
create_dir_all(&base).unwrap();
1482+
let expected_file = base.join("pyproject.toml");
1483+
save_pyproject_toml_file(&project_info).unwrap();
1484+
1485+
assert!(expected_file.is_file());
1486+
1487+
let content = std::fs::read_to_string(expected_file).unwrap();
1488+
1489+
assert_yaml_snapshot!(content);
1490+
}
1491+
13661492
#[test]
13671493
fn test_save_setuptools_pyproject_toml_file_apache_application() {
13681494
let mut project_info = project_info_dummy();
@@ -1435,6 +1561,48 @@ mod tests {
14351561
assert_yaml_snapshot!(content);
14361562
}
14371563

1564+
#[test]
1565+
fn test_save_pyproject_toml_file_uv_application_with_python_extras() {
1566+
let mut project_info = project_info_dummy();
1567+
project_info.project_manager = ProjectManager::Uv;
1568+
project_info.is_application = true;
1569+
project_info.extra_python_packages = Some(vec![
1570+
"[email protected]".to_string(),
1571+
"[email protected]".to_string(),
1572+
]);
1573+
let base = project_info.base_dir();
1574+
create_dir_all(&base).unwrap();
1575+
let expected_file = base.join("pyproject.toml");
1576+
save_pyproject_toml_file(&project_info).unwrap();
1577+
1578+
assert!(expected_file.is_file());
1579+
1580+
let content = std::fs::read_to_string(expected_file).unwrap();
1581+
1582+
assert_yaml_snapshot!(content);
1583+
}
1584+
1585+
#[test]
1586+
fn test_save_pyproject_toml_file_uv_lib_with_python_extras() {
1587+
let mut project_info = project_info_dummy();
1588+
project_info.project_manager = ProjectManager::Uv;
1589+
project_info.is_application = false;
1590+
project_info.extra_python_packages = Some(vec![
1591+
"[email protected]".to_string(),
1592+
"[email protected]".to_string(),
1593+
]);
1594+
let base = project_info.base_dir();
1595+
create_dir_all(&base).unwrap();
1596+
let expected_file = base.join("pyproject.toml");
1597+
save_pyproject_toml_file(&project_info).unwrap();
1598+
1599+
assert!(expected_file.is_file());
1600+
1601+
let content = std::fs::read_to_string(expected_file).unwrap();
1602+
1603+
assert_yaml_snapshot!(content);
1604+
}
1605+
14381606
#[test]
14391607
fn test_save_uv_pyproject_toml_file_apache_application() {
14401608
let mut project_info = project_info_dummy();
@@ -1507,6 +1675,48 @@ mod tests {
15071675
assert_yaml_snapshot!(content);
15081676
}
15091677

1678+
#[test]
1679+
fn test_save_pyproject_toml_file_pixi_application_with_python_extras() {
1680+
let mut project_info = project_info_dummy();
1681+
project_info.project_manager = ProjectManager::Pixi;
1682+
project_info.is_application = true;
1683+
project_info.extra_python_packages = Some(vec![
1684+
"[email protected]".to_string(),
1685+
"[email protected]".to_string(),
1686+
]);
1687+
let base = project_info.base_dir();
1688+
create_dir_all(&base).unwrap();
1689+
let expected_file = base.join("pyproject.toml");
1690+
save_pyproject_toml_file(&project_info).unwrap();
1691+
1692+
assert!(expected_file.is_file());
1693+
1694+
let content = std::fs::read_to_string(expected_file).unwrap();
1695+
1696+
assert_yaml_snapshot!(content);
1697+
}
1698+
1699+
#[test]
1700+
fn test_save_pyproject_toml_file_pixi_lib_with_python_extras() {
1701+
let mut project_info = project_info_dummy();
1702+
project_info.project_manager = ProjectManager::Pixi;
1703+
project_info.is_application = false;
1704+
project_info.extra_python_packages = Some(vec![
1705+
"[email protected]".to_string(),
1706+
"[email protected]".to_string(),
1707+
]);
1708+
let base = project_info.base_dir();
1709+
create_dir_all(&base).unwrap();
1710+
let expected_file = base.join("pyproject.toml");
1711+
save_pyproject_toml_file(&project_info).unwrap();
1712+
1713+
assert!(expected_file.is_file());
1714+
1715+
let content = std::fs::read_to_string(expected_file).unwrap();
1716+
1717+
assert_yaml_snapshot!(content);
1718+
}
1719+
15101720
#[test]
15111721
fn test_save_pixi_pyproject_toml_file_apache_application() {
15121722
let mut project_info = project_info_dummy();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
source: src/project_generator.rs
3+
expression: content
4+
---
5+
"[tool.poetry]\nname = \"my-project\"\nversion = \"0.1.7\"\ndescription = \"This is a test\"\nauthors = [\"Arthur Dent <[email protected]>\"]\nlicense = \"MIT\"\nreadme = \"README.md\"\n\n[tool.poetry.dependencies]\npython = \"^3.9\"\nfastapi = \"0.115.0\"\ncamel-converter = \"4.0.0\"\n\n[tool.poetry.group.dev.dependencies]\nmypy = \"1.11.2\"\npre-commit = \"3.8.0\"\npytest = \"8.3.3\"\npytest-cov = \"5.0.0\"\nruff = \"0.6.9\"\ntomli = {version = \"2.0.1\", python = \"<3.11\"}\n\n[build-system]\nrequires = [\"poetry-core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n\n[tool.mypy]\ncheck_untyped_defs = true\ndisallow_untyped_defs = true\n\n[[tool.mypy.overrides]]\nmodule = [\"tests.*\"]\ndisallow_untyped_defs = false\n\n[tool.pytest.ini_options]\nminversion = \"6.0\"\naddopts = \"--cov=my_project --cov-report term-missing --no-cov-on-fail\"\n\n[tool.coverage.report]\nexclude_lines = [\"if __name__ == .__main__.:\", \"pragma: no cover\"]\n\n[tool.ruff]\nline-length = 100\ntarget-version = \"py39\"\nfix = true\n\n[tool.ruff.lint]\nselect = [\n \"E\", # pycodestyle\n \"B\", # flake8-bugbear\n \"W\", # Warning\n \"F\", # pyflakes\n \"UP\", # pyupgrade\n \"I001\", # unsorted-imports\n \"T201\", # print found\n \"T203\" # pprint found\n]\nignore=[\n # Recommended ignores by ruff when using formatter\n \"E501\",\n \"W191\",\n \"E111\",\n \"E114\",\n \"E117\",\n \"D206\",\n \"D300\",\n \"Q000\",\n \"Q001\",\n \"Q002\",\n \"Q003\",\n \"COM812\",\n \"COM819\",\n \"ISC001\",\n \"ISC002\",\n]\n"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
source: src/project_generator.rs
3+
expression: content
4+
---
5+
"[tool.poetry]\nname = \"my-project\"\nversion = \"0.1.7\"\ndescription = \"This is a test\"\nauthors = [\"Arthur Dent <[email protected]>\"]\nlicense = \"MIT\"\nreadme = \"README.md\"\n\n[tool.poetry.dependencies]\npython = \"^3.9\"\nfastapi = \">=0.115.0\"\ncamel-converter = \">=4.0.0\"\n\n[tool.poetry.group.dev.dependencies]\nmypy = \"1.11.2\"\npre-commit = \"3.8.0\"\npytest = \"8.3.3\"\npytest-cov = \"5.0.0\"\nruff = \"0.6.9\"\ntomli = {version = \"2.0.1\", python = \"<3.11\"}\n\n[build-system]\nrequires = [\"poetry-core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n\n[tool.mypy]\ncheck_untyped_defs = true\ndisallow_untyped_defs = true\n\n[[tool.mypy.overrides]]\nmodule = [\"tests.*\"]\ndisallow_untyped_defs = false\n\n[tool.pytest.ini_options]\nminversion = \"6.0\"\naddopts = \"--cov=my_project --cov-report term-missing --no-cov-on-fail\"\n\n[tool.coverage.report]\nexclude_lines = [\"if __name__ == .__main__.:\", \"pragma: no cover\"]\n\n[tool.ruff]\nline-length = 100\ntarget-version = \"py39\"\nfix = true\n\n[tool.ruff.lint]\nselect = [\n \"E\", # pycodestyle\n \"B\", # flake8-bugbear\n \"W\", # Warning\n \"F\", # pyflakes\n \"UP\", # pyupgrade\n \"I001\", # unsorted-imports\n \"T201\", # print found\n \"T203\" # pprint found\n]\nignore=[\n # Recommended ignores by ruff when using formatter\n \"E501\",\n \"W191\",\n \"E111\",\n \"E114\",\n \"E117\",\n \"D206\",\n \"D300\",\n \"Q000\",\n \"Q001\",\n \"Q002\",\n \"Q003\",\n \"COM812\",\n \"COM819\",\n \"ISC001\",\n \"ISC002\",\n]\n"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
source: src/project_generator.rs
3+
expression: content
4+
---
5+
"[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"my-project\"\ndescription = \"This is a test\"\nauthors = [\n { name = \"Arthur Dent\", email = \"[email protected]\" }\n]\nlicense = { file = \"LICENSE\" }\nreadme = \"README.md\"\nrequires-python = \">=3.9\"\ndynamic = [\"version\"]\ndependencies = [\n \"fastapi==0.115.0\",\n \"camel-converter==4.0.0\",\n]\n\n[tool.pixi.project]\nchannels = [\"conda-forge\", \"bioconda\"]\nplatforms = [\"linux-64\", \"osx-arm64\", \"osx-64\", \"win-64\"]\n\n[tool.pixi.feature.dev.tasks]\nrun-mypy = \"mypy my_project tests\"\nrun-ruff-check = \"ruff check my_project tests\"\nrun-ruff-format = \"ruff format my_project tests\"\nrun-pytest = \"pytest -x\"\n\n\n[project.optional-dependencies]\ndev = [\n \"mypy==1.11.2\",\n \"pre-commit==3.8.0\",\n \"pytest==8.3.3\",\n \"pytest-cov==5.0.0\",\n \"ruff==0.6.9\",\n]\n\n[tool.pixi.environments]\ndefault = {features = [], solve-group = \"default\"}\ndev = {features = [\"dev\"], solve-group = \"default\"}\n\n[tool.hatch.version]\npath = \"my_project/_version.py\"\n\n[tool.mypy]\ncheck_untyped_defs = true\ndisallow_untyped_defs = true\n\n[[tool.mypy.overrides]]\nmodule = [\"tests.*\"]\ndisallow_untyped_defs = false\n\n[tool.pytest.ini_options]\nminversion = \"6.0\"\naddopts = \"--cov=my_project --cov-report term-missing --no-cov-on-fail\"\n\n[tool.coverage.report]\nexclude_lines = [\"if __name__ == .__main__.:\", \"pragma: no cover\"]\n\n[tool.ruff]\nline-length = 100\ntarget-version = \"py39\"\nfix = true\n\n[tool.ruff.lint]\nselect = [\n \"E\", # pycodestyle\n \"B\", # flake8-bugbear\n \"W\", # Warning\n \"F\", # pyflakes\n \"UP\", # pyupgrade\n \"I001\", # unsorted-imports\n \"T201\", # print found\n \"T203\" # pprint found\n]\nignore=[\n # Recommended ignores by ruff when using formatter\n \"E501\",\n \"W191\",\n \"E111\",\n \"E114\",\n \"E117\",\n \"D206\",\n \"D300\",\n \"Q000\",\n \"Q001\",\n \"Q002\",\n \"Q003\",\n \"COM812\",\n \"COM819\",\n \"ISC001\",\n \"ISC002\",\n]\n"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
source: src/project_generator.rs
3+
expression: content
4+
---
5+
"[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[project]\nname = \"my-project\"\ndescription = \"This is a test\"\nauthors = [\n { name = \"Arthur Dent\", email = \"[email protected]\" }\n]\nlicense = { file = \"LICENSE\" }\nreadme = \"README.md\"\nrequires-python = \">=3.9\"\ndynamic = [\"version\"]\ndependencies = [\n \"fastapi>=0.115.0\",\n \"camel-converter>=4.0.0\",\n]\n\n[tool.pixi.project]\nchannels = [\"conda-forge\", \"bioconda\"]\nplatforms = [\"linux-64\", \"osx-arm64\", \"osx-64\", \"win-64\"]\n\n[tool.pixi.feature.dev.tasks]\nrun-mypy = \"mypy my_project tests\"\nrun-ruff-check = \"ruff check my_project tests\"\nrun-ruff-format = \"ruff format my_project tests\"\nrun-pytest = \"pytest -x\"\n\n\n[project.optional-dependencies]\ndev = [\n \"mypy==1.11.2\",\n \"pre-commit==3.8.0\",\n \"pytest==8.3.3\",\n \"pytest-cov==5.0.0\",\n \"ruff==0.6.9\",\n]\n\n[tool.pixi.environments]\ndefault = {features = [], solve-group = \"default\"}\ndev = {features = [\"dev\"], solve-group = \"default\"}\n\n[tool.hatch.version]\npath = \"my_project/_version.py\"\n\n[tool.mypy]\ncheck_untyped_defs = true\ndisallow_untyped_defs = true\n\n[[tool.mypy.overrides]]\nmodule = [\"tests.*\"]\ndisallow_untyped_defs = false\n\n[tool.pytest.ini_options]\nminversion = \"6.0\"\naddopts = \"--cov=my_project --cov-report term-missing --no-cov-on-fail\"\n\n[tool.coverage.report]\nexclude_lines = [\"if __name__ == .__main__.:\", \"pragma: no cover\"]\n\n[tool.ruff]\nline-length = 100\ntarget-version = \"py39\"\nfix = true\n\n[tool.ruff.lint]\nselect = [\n \"E\", # pycodestyle\n \"B\", # flake8-bugbear\n \"W\", # Warning\n \"F\", # pyflakes\n \"UP\", # pyupgrade\n \"I001\", # unsorted-imports\n \"T201\", # print found\n \"T203\" # pprint found\n]\nignore=[\n # Recommended ignores by ruff when using formatter\n \"E501\",\n \"W191\",\n \"E111\",\n \"E114\",\n \"E117\",\n \"D206\",\n \"D300\",\n \"Q000\",\n \"Q001\",\n \"Q002\",\n \"Q003\",\n \"COM812\",\n \"COM819\",\n \"ISC001\",\n \"ISC002\",\n]\n"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
source: src/project_generator.rs
3+
expression: content
4+
---
5+
"[build-system]\nrequires = [\"maturin>=1.5,<2.0\"]\nbuild-backend = \"maturin\"\n\n[project]\nname = \"my-project\"\ndescription = \"This is a test\"\nauthors = [{name = \"Arthur Dent\", email = \"[email protected]\"}]\nlicense = \"MIT\"\nreadme = \"README.md\"\ndependencies = [\n \"fastapi==0.115.0\",\n \"camel-converter==4.0.0\",\n]\n\n[tool.maturin]\nmodule-name = \"my_project._my_project\"\nbinding = \"pyo3\"\nfeatures = [\"pyo3/extension-module\"]\n\n[tool.mypy]\ncheck_untyped_defs = true\ndisallow_untyped_defs = true\n\n[[tool.mypy.overrides]]\nmodule = [\"tests.*\"]\ndisallow_untyped_defs = false\n\n[tool.pytest.ini_options]\nminversion = \"6.0\"\naddopts = \"--cov=my_project --cov-report term-missing --no-cov-on-fail\"\n\n[tool.coverage.report]\nexclude_lines = [\"if __name__ == .__main__.:\", \"pragma: no cover\"]\n\n[tool.ruff]\nline-length = 100\ntarget-version = \"py39\"\nfix = true\n\n[tool.ruff.lint]\nselect = [\n \"E\", # pycodestyle\n \"B\", # flake8-bugbear\n \"W\", # Warning\n \"F\", # pyflakes\n \"UP\", # pyupgrade\n \"I001\", # unsorted-imports\n \"T201\", # print found\n \"T203\" # pprint found\n]\nignore=[\n # Recommended ignores by ruff when using formatter\n \"E501\",\n \"W191\",\n \"E111\",\n \"E114\",\n \"E117\",\n \"D206\",\n \"D300\",\n \"Q000\",\n \"Q001\",\n \"Q002\",\n \"Q003\",\n \"COM812\",\n \"COM819\",\n \"ISC001\",\n \"ISC002\",\n]\n"

0 commit comments

Comments
 (0)