1use std::collections::{HashMap, HashSet, VecDeque};
2use std::path::Path;
3
4use log::warn;
5use packageurl::PackageUrl;
6use regex::Regex;
7use serde_json::{Map as JsonMap, Value as JsonValue};
8use toml::Value as TomlValue;
9use toml::map::Map as TomlMap;
10
11use crate::models::{DatasourceId, Dependency, PackageData, PackageType, ResolvedPackage};
12use crate::parsers::python::read_toml_file;
13
14use super::PackageParser;
15
16const FIELD_LOCK_VERSION: &str = "lock-version";
17const FIELD_CREATED_BY: &str = "created-by";
18const SUPPORTED_LOCK_VERSION: &str = "1.0";
19const FIELD_REQUIRES_PYTHON: &str = "requires-python";
20const FIELD_ENVIRONMENTS: &str = "environments";
21const FIELD_EXTRAS: &str = "extras";
22const FIELD_DEPENDENCY_GROUPS: &str = "dependency-groups";
23const FIELD_DEFAULT_GROUPS: &str = "default-groups";
24const FIELD_PACKAGES: &str = "packages";
25const FIELD_NAME: &str = "name";
26const FIELD_VERSION: &str = "version";
27const FIELD_MARKER: &str = "marker";
28const FIELD_DEPENDENCIES: &str = "dependencies";
29const FIELD_INDEX: &str = "index";
30const FIELD_VCS: &str = "vcs";
31const FIELD_DIRECTORY: &str = "directory";
32const FIELD_ARCHIVE: &str = "archive";
33const FIELD_SDIST: &str = "sdist";
34const FIELD_WHEELS: &str = "wheels";
35const FIELD_HASHES: &str = "hashes";
36const FIELD_TOOL: &str = "tool";
37const FIELD_ATTESTATION_IDENTITIES: &str = "attestation-identities";
38
39pub struct PylockTomlParser;
40
41#[derive(Clone, Debug, Default)]
42struct MarkerClassification {
43 is_runtime: bool,
44 is_optional: bool,
45 scope: Option<String>,
46}
47
48struct DependencyAnalysisContext<'a> {
49 package_tables: &'a [&'a TomlMap<String, TomlValue>],
50 dependency_indices: &'a [Vec<usize>],
51 incoming_counts: &'a [usize],
52 root_classifications: &'a [MarkerClassification],
53 runtime_reachable: &'a HashSet<usize>,
54 optional_reachable: &'a HashSet<usize>,
55 scope_sets: &'a HashMap<String, HashSet<usize>>,
56}
57
58impl PackageParser for PylockTomlParser {
59 const PACKAGE_TYPE: PackageType = PackageType::Pypi;
60
61 fn is_match(path: &Path) -> bool {
62 let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
63 return false;
64 };
65
66 file_name == "pylock.toml"
67 || file_name
68 .strip_prefix("pylock.")
69 .and_then(|suffix| suffix.strip_suffix(".toml"))
70 .is_some_and(|middle| !middle.is_empty() && !middle.contains('.'))
71 }
72
73 fn extract_packages(path: &Path) -> Vec<PackageData> {
74 let toml_content = match read_toml_file(path) {
75 Ok(content) => content,
76 Err(e) => {
77 warn!("Failed to read pylock.toml at {:?}: {}", path, e);
78 return vec![default_package_data()];
79 }
80 };
81
82 vec![parse_pylock_toml(&toml_content)]
83 }
84}
85
86fn parse_pylock_toml(toml_content: &TomlValue) -> PackageData {
87 let lock_version = toml_content
88 .get(FIELD_LOCK_VERSION)
89 .and_then(TomlValue::as_str);
90 if lock_version != Some(SUPPORTED_LOCK_VERSION) {
91 warn!(
92 "Invalid pylock.toml: missing or unsupported lock-version {:?}",
93 lock_version
94 );
95 return default_package_data();
96 }
97
98 let created_by = toml_content
99 .get(FIELD_CREATED_BY)
100 .and_then(TomlValue::as_str);
101 if created_by.is_none() {
102 warn!("Invalid pylock.toml: missing required created-by field");
103 return default_package_data();
104 }
105
106 let Some(package_values) = toml_content
107 .get(FIELD_PACKAGES)
108 .and_then(TomlValue::as_array)
109 else {
110 warn!("Invalid pylock.toml: missing required packages array");
111 return default_package_data();
112 };
113
114 let package_tables: Vec<&TomlMap<String, TomlValue>> = package_values
115 .iter()
116 .filter_map(TomlValue::as_table)
117 .collect();
118 if package_tables.is_empty() {
119 warn!("Invalid pylock.toml: packages array does not contain package tables");
120 return default_package_data();
121 }
122
123 let dependency_indices = build_dependency_indices(&package_tables);
124 let incoming_counts = build_incoming_counts(package_tables.len(), &dependency_indices);
125 let default_groups = extract_string_set(toml_content, FIELD_DEFAULT_GROUPS);
126
127 let root_classifications: Vec<MarkerClassification> = package_tables
128 .iter()
129 .enumerate()
130 .map(|(index, table)| {
131 if incoming_counts[index] == 0 {
132 classify_marker(
133 table.get(FIELD_MARKER).and_then(TomlValue::as_str),
134 &default_groups,
135 )
136 } else {
137 MarkerClassification::default()
138 }
139 })
140 .collect();
141
142 let runtime_roots: Vec<usize> = root_classifications
143 .iter()
144 .enumerate()
145 .filter_map(|(index, info)| {
146 (incoming_counts[index] == 0 && info.is_runtime).then_some(index)
147 })
148 .collect();
149 let optional_roots: Vec<usize> = root_classifications
150 .iter()
151 .enumerate()
152 .filter_map(|(index, info)| {
153 (incoming_counts[index] == 0 && info.is_optional).then_some(index)
154 })
155 .collect();
156
157 let runtime_reachable = collect_reachable_indices(&dependency_indices, &runtime_roots);
158 let optional_reachable = collect_reachable_indices(&dependency_indices, &optional_roots);
159
160 let mut scope_sets: HashMap<String, HashSet<usize>> = HashMap::new();
161 for (index, info) in root_classifications.iter().enumerate() {
162 if incoming_counts[index] != 0 {
163 continue;
164 }
165
166 if let Some(scope) = info.scope.as_ref() {
167 scope_sets.insert(
168 scope.clone(),
169 collect_reachable_indices(&dependency_indices, &[index]),
170 );
171 }
172 }
173
174 let analysis = DependencyAnalysisContext {
175 package_tables: &package_tables,
176 dependency_indices: &dependency_indices,
177 incoming_counts: &incoming_counts,
178 root_classifications: &root_classifications,
179 runtime_reachable: &runtime_reachable,
180 optional_reachable: &optional_reachable,
181 scope_sets: &scope_sets,
182 };
183
184 let mut package_data = default_package_data();
185 package_data.extra_data = build_lock_extra_data(toml_content);
186 package_data.dependencies = package_tables
187 .iter()
188 .enumerate()
189 .filter_map(|(index, package_table)| {
190 build_top_level_dependency(index, package_table, &analysis)
191 })
192 .collect();
193
194 package_data
195}
196
197fn build_top_level_dependency(
198 index: usize,
199 package_table: &TomlMap<String, TomlValue>,
200 analysis: &DependencyAnalysisContext<'_>,
201) -> Option<Dependency> {
202 let name = normalized_package_name(package_table)?;
203 let version = package_version(package_table);
204 let direct = analysis
205 .incoming_counts
206 .get(index)
207 .copied()
208 .unwrap_or_default()
209 == 0;
210
211 let (is_runtime, is_optional, scope) = if direct {
212 let classification = analysis
213 .root_classifications
214 .get(index)
215 .cloned()
216 .unwrap_or_default();
217 (
218 classification.is_runtime,
219 classification.is_optional,
220 classification.scope,
221 )
222 } else {
223 let is_runtime = analysis.runtime_reachable.contains(&index);
224 let is_optional = !is_runtime && analysis.optional_reachable.contains(&index);
225 let scope = scope_for_index(analysis.scope_sets, index);
226 (is_runtime, is_optional, scope)
227 };
228
229 Some(Dependency {
230 purl: create_pypi_purl(&name, version.as_deref()),
231 extracted_requirement: None,
232 scope,
233 is_runtime: Some(is_runtime),
234 is_optional: Some(is_optional),
235 is_pinned: Some(is_package_pinned(package_table)),
236 is_direct: Some(direct),
237 resolved_package: Some(Box::new(build_resolved_package(
238 package_table,
239 analysis.package_tables,
240 analysis
241 .dependency_indices
242 .get(index)
243 .map(Vec::as_slice)
244 .unwrap_or(&[]),
245 ))),
246 extra_data: build_package_extra_data(package_table),
247 })
248}
249
250fn build_resolved_package(
251 package_table: &TomlMap<String, TomlValue>,
252 package_tables: &[&TomlMap<String, TomlValue>],
253 dependency_indices: &[usize],
254) -> ResolvedPackage {
255 let name = normalized_package_name(package_table).unwrap_or_default();
256 let version = package_version(package_table).unwrap_or_default();
257 let (_, repository_download_url, api_data_url, purl) = build_pypi_urls(
258 Some(&name),
259 (!version.is_empty()).then_some(version.as_str()),
260 );
261 let repository_homepage_url = Some(format!("https://pypi.org/project/{}", name));
262 let (download_url, sha256, sha512, md5) = extract_artifact_metadata(package_table);
263
264 ResolvedPackage {
265 package_type: PylockTomlParser::PACKAGE_TYPE,
266 namespace: String::new(),
267 name,
268 version,
269 primary_language: Some("Python".to_string()),
270 download_url,
271 sha1: None,
272 sha256,
273 sha512,
274 md5,
275 is_virtual: false,
276 extra_data: build_package_extra_data(package_table),
277 dependencies: dependency_indices
278 .iter()
279 .filter_map(|child_index| package_tables.get(*child_index))
280 .filter_map(|child| build_resolved_dependency(child))
281 .collect(),
282 repository_homepage_url,
283 repository_download_url,
284 api_data_url,
285 datasource_id: Some(DatasourceId::PypiPylockToml),
286 purl,
287 }
288}
289
290fn build_resolved_dependency(package_table: &TomlMap<String, TomlValue>) -> Option<Dependency> {
291 let name = normalized_package_name(package_table)?;
292 let version = package_version(package_table);
293
294 Some(Dependency {
295 purl: create_pypi_purl(&name, version.as_deref()),
296 extracted_requirement: None,
297 scope: None,
298 is_runtime: None,
299 is_optional: None,
300 is_pinned: Some(is_package_pinned(package_table)),
301 is_direct: Some(true),
302 resolved_package: None,
303 extra_data: build_package_extra_data(package_table),
304 })
305}
306
307fn build_dependency_indices(package_tables: &[&TomlMap<String, TomlValue>]) -> Vec<Vec<usize>> {
308 package_tables
309 .iter()
310 .map(|package_table| {
311 package_table
312 .get(FIELD_DEPENDENCIES)
313 .and_then(TomlValue::as_array)
314 .into_iter()
315 .flatten()
316 .filter_map(TomlValue::as_table)
317 .flat_map(|reference| {
318 resolve_dependency_reference_indices(package_tables, reference)
319 })
320 .collect()
321 })
322 .collect()
323}
324
325fn resolve_dependency_reference_indices(
326 package_tables: &[&TomlMap<String, TomlValue>],
327 reference: &TomlMap<String, TomlValue>,
328) -> Vec<usize> {
329 let matches: Vec<usize> = package_tables
330 .iter()
331 .enumerate()
332 .filter_map(|(index, package_table)| {
333 package_reference_matches(package_table, reference).then_some(index)
334 })
335 .collect();
336
337 if matches.len() == 1 {
338 matches
339 } else {
340 Vec::new()
341 }
342}
343
344fn package_reference_matches(
345 package_table: &TomlMap<String, TomlValue>,
346 reference: &TomlMap<String, TomlValue>,
347) -> bool {
348 reference.iter().all(|(key, ref_value)| {
349 package_table
350 .get(key)
351 .is_some_and(|pkg_value| toml_values_match(pkg_value, ref_value))
352 })
353}
354
355fn toml_values_match(left: &TomlValue, right: &TomlValue) -> bool {
356 match (left, right) {
357 (TomlValue::String(left), TomlValue::String(right)) => left == right,
358 (TomlValue::Integer(left), TomlValue::Integer(right)) => left == right,
359 (TomlValue::Float(left), TomlValue::Float(right)) => left == right,
360 (TomlValue::Boolean(left), TomlValue::Boolean(right)) => left == right,
361 (TomlValue::Datetime(left), TomlValue::Datetime(right)) => left == right,
362 (TomlValue::Array(left), TomlValue::Array(right)) => {
363 left.len() == right.len()
364 && left
365 .iter()
366 .zip(right.iter())
367 .all(|(left, right)| toml_values_match(left, right))
368 }
369 (TomlValue::Table(left), TomlValue::Table(right)) => {
370 right.iter().all(|(key, right_value)| {
371 left.get(key)
372 .is_some_and(|left_value| toml_values_match(left_value, right_value))
373 })
374 }
375 _ => false,
376 }
377}
378
379fn build_incoming_counts(package_count: usize, dependency_indices: &[Vec<usize>]) -> Vec<usize> {
380 let mut incoming = vec![0; package_count];
381 for dependency_list in dependency_indices {
382 for &child_index in dependency_list {
383 if let Some(count) = incoming.get_mut(child_index) {
384 *count += 1;
385 }
386 }
387 }
388 incoming
389}
390
391fn collect_reachable_indices(dependency_indices: &[Vec<usize>], roots: &[usize]) -> HashSet<usize> {
392 let mut visited = HashSet::new();
393 let mut queue: VecDeque<usize> = roots.iter().copied().collect();
394
395 while let Some(index) = queue.pop_front() {
396 if !visited.insert(index) {
397 continue;
398 }
399
400 for &child_index in dependency_indices.get(index).into_iter().flatten() {
401 queue.push_back(child_index);
402 }
403 }
404
405 visited
406}
407
408fn classify_marker(marker: Option<&str>, default_groups: &HashSet<String>) -> MarkerClassification {
409 let Some(marker) = marker else {
410 return MarkerClassification {
411 is_runtime: true,
412 is_optional: false,
413 scope: None,
414 };
415 };
416
417 let extras = extract_marker_memberships(marker, "extras");
418 if !extras.is_empty() {
419 return MarkerClassification {
420 is_runtime: false,
421 is_optional: true,
422 scope: single_scope(extras),
423 };
424 }
425
426 let groups = extract_marker_memberships(marker, "dependency_groups");
427 let non_default_groups: Vec<String> = groups
428 .into_iter()
429 .filter(|group| !default_groups.contains(group))
430 .collect();
431 if !non_default_groups.is_empty() {
432 return MarkerClassification {
433 is_runtime: false,
434 is_optional: false,
435 scope: single_scope(non_default_groups),
436 };
437 }
438
439 MarkerClassification {
440 is_runtime: true,
441 is_optional: false,
442 scope: None,
443 }
444}
445
446fn extract_marker_memberships(marker: &str, variable_name: &str) -> Vec<String> {
447 let pattern = format!(
448 r#"['\"]([^'\"]+)['\"]\s+in\s+{}\b"#,
449 regex::escape(variable_name)
450 );
451 let Ok(regex) = Regex::new(&pattern) else {
452 return Vec::new();
453 };
454
455 let mut memberships: Vec<String> = regex
456 .captures_iter(marker)
457 .filter_map(|captures| {
458 captures
459 .get(1)
460 .map(|value| value.as_str().trim().to_string())
461 })
462 .filter(|value| !value.is_empty())
463 .collect();
464 memberships.sort();
465 memberships.dedup();
466 memberships
467}
468
469fn single_scope(values: Vec<String>) -> Option<String> {
470 (values.len() == 1).then(|| values[0].clone())
471}
472
473fn scope_for_index(scope_sets: &HashMap<String, HashSet<usize>>, index: usize) -> Option<String> {
474 let matches: Vec<String> = scope_sets
475 .iter()
476 .filter_map(|(scope, indices)| indices.contains(&index).then_some(scope.clone()))
477 .collect();
478 single_scope(matches)
479}
480
481fn normalized_package_name(package_table: &TomlMap<String, TomlValue>) -> Option<String> {
482 package_table
483 .get(FIELD_NAME)
484 .and_then(TomlValue::as_str)
485 .map(|value| value.trim().to_ascii_lowercase())
486}
487
488fn package_version(package_table: &TomlMap<String, TomlValue>) -> Option<String> {
489 package_table
490 .get(FIELD_VERSION)
491 .and_then(TomlValue::as_str)
492 .map(|value| value.to_string())
493}
494
495fn is_package_pinned(package_table: &TomlMap<String, TomlValue>) -> bool {
496 package_table.contains_key(FIELD_VERSION)
497 || package_table
498 .get(FIELD_VCS)
499 .and_then(TomlValue::as_table)
500 .is_some_and(|table| table.contains_key("commit-id"))
501 || has_hashes(package_table.get(FIELD_ARCHIVE))
502 || has_hashes(package_table.get(FIELD_SDIST))
503 || package_table
504 .get(FIELD_WHEELS)
505 .and_then(TomlValue::as_array)
506 .into_iter()
507 .flatten()
508 .filter_map(TomlValue::as_table)
509 .any(|wheel| wheel.contains_key(FIELD_HASHES))
510}
511
512fn has_hashes(value: Option<&TomlValue>) -> bool {
513 value
514 .and_then(TomlValue::as_table)
515 .is_some_and(|table| table.contains_key(FIELD_HASHES))
516}
517
518fn build_lock_extra_data(toml_content: &TomlValue) -> Option<HashMap<String, JsonValue>> {
519 let mut extra_data = HashMap::new();
520
521 for (source_key, target_key) in [
522 (FIELD_LOCK_VERSION, "lock_version"),
523 (FIELD_CREATED_BY, "created_by"),
524 (FIELD_REQUIRES_PYTHON, "requires_python"),
525 (FIELD_ENVIRONMENTS, FIELD_ENVIRONMENTS),
526 (FIELD_EXTRAS, FIELD_EXTRAS),
527 (FIELD_DEPENDENCY_GROUPS, FIELD_DEPENDENCY_GROUPS),
528 (FIELD_DEFAULT_GROUPS, FIELD_DEFAULT_GROUPS),
529 ] {
530 if let Some(value) = toml_content.get(source_key) {
531 extra_data.insert(target_key.to_string(), toml_value_to_json(value));
532 }
533 }
534
535 if let Some(tool) = toml_content.get(FIELD_TOOL) {
536 extra_data.insert(FIELD_TOOL.to_string(), toml_value_to_json(tool));
537 }
538
539 (!extra_data.is_empty()).then_some(extra_data)
540}
541
542fn build_package_extra_data(
543 package_table: &TomlMap<String, TomlValue>,
544) -> Option<HashMap<String, JsonValue>> {
545 let mut extra_data = HashMap::new();
546
547 for key in [
548 FIELD_MARKER,
549 FIELD_REQUIRES_PYTHON,
550 FIELD_INDEX,
551 FIELD_VCS,
552 FIELD_DIRECTORY,
553 FIELD_ARCHIVE,
554 FIELD_SDIST,
555 FIELD_WHEELS,
556 FIELD_TOOL,
557 FIELD_ATTESTATION_IDENTITIES,
558 ] {
559 if let Some(value) = package_table.get(key) {
560 extra_data.insert(key.to_string(), toml_value_to_json(value));
561 }
562 }
563
564 (!extra_data.is_empty()).then_some(extra_data)
565}
566
567fn extract_artifact_metadata(
568 package_table: &TomlMap<String, TomlValue>,
569) -> (
570 Option<String>,
571 Option<String>,
572 Option<String>,
573 Option<String>,
574) {
575 if let Some(archive_table) = package_table
576 .get(FIELD_ARCHIVE)
577 .and_then(TomlValue::as_table)
578 {
579 return (
580 archive_table
581 .get("url")
582 .and_then(TomlValue::as_str)
583 .map(|value| value.to_string())
584 .or_else(|| {
585 archive_table
586 .get("path")
587 .and_then(TomlValue::as_str)
588 .map(|value| value.to_string())
589 }),
590 extract_hash_by_name(archive_table, "sha256"),
591 extract_hash_by_name(archive_table, "sha512"),
592 extract_hash_by_name(archive_table, "md5"),
593 );
594 }
595
596 if let Some(sdist_table) = package_table.get(FIELD_SDIST).and_then(TomlValue::as_table) {
597 return (
598 sdist_table
599 .get("url")
600 .and_then(TomlValue::as_str)
601 .map(|value| value.to_string())
602 .or_else(|| {
603 sdist_table
604 .get("path")
605 .and_then(TomlValue::as_str)
606 .map(|value| value.to_string())
607 }),
608 extract_hash_by_name(sdist_table, "sha256"),
609 extract_hash_by_name(sdist_table, "sha512"),
610 extract_hash_by_name(sdist_table, "md5"),
611 );
612 }
613
614 let wheel_table = package_table
615 .get(FIELD_WHEELS)
616 .and_then(TomlValue::as_array)
617 .and_then(|wheels| wheels.first())
618 .and_then(TomlValue::as_table);
619
620 (
621 wheel_table
622 .and_then(|table| table.get("url"))
623 .and_then(TomlValue::as_str)
624 .map(|value| value.to_string())
625 .or_else(|| {
626 wheel_table
627 .and_then(|table| table.get("path"))
628 .and_then(TomlValue::as_str)
629 .map(|value| value.to_string())
630 }),
631 wheel_table.and_then(|table| extract_hash_by_name(table, "sha256")),
632 wheel_table.and_then(|table| extract_hash_by_name(table, "sha512")),
633 wheel_table.and_then(|table| extract_hash_by_name(table, "md5")),
634 )
635}
636
637fn extract_hash_by_name(table: &TomlMap<String, TomlValue>, name: &str) -> Option<String> {
638 table
639 .get(FIELD_HASHES)
640 .and_then(TomlValue::as_table)
641 .and_then(|hashes| hashes.get(name))
642 .and_then(TomlValue::as_str)
643 .map(|value| value.to_string())
644}
645
646fn extract_string_set(toml_content: &TomlValue, key: &str) -> HashSet<String> {
647 toml_content
648 .get(key)
649 .and_then(TomlValue::as_array)
650 .into_iter()
651 .flatten()
652 .filter_map(TomlValue::as_str)
653 .map(|value| value.to_string())
654 .collect()
655}
656
657fn build_pypi_urls(
658 name: Option<&str>,
659 version: Option<&str>,
660) -> (
661 Option<String>,
662 Option<String>,
663 Option<String>,
664 Option<String>,
665) {
666 let repository_homepage_url = name.map(|value| format!("https://pypi.org/project/{}", value));
667 let repository_download_url = name.and_then(|value| {
668 version.map(|ver| {
669 format!(
670 "https://pypi.org/packages/source/{}/{}/{}-{}.tar.gz",
671 &value[..1.min(value.len())],
672 value,
673 value,
674 ver
675 )
676 })
677 });
678 let api_data_url = name.map(|value| {
679 if let Some(ver) = version {
680 format!("https://pypi.org/pypi/{}/{}/json", value, ver)
681 } else {
682 format!("https://pypi.org/pypi/{}/json", value)
683 }
684 });
685 let purl = name.and_then(|value| create_pypi_purl(value, version));
686
687 (
688 repository_homepage_url,
689 repository_download_url,
690 api_data_url,
691 purl,
692 )
693}
694
695fn create_pypi_purl(name: &str, version: Option<&str>) -> Option<String> {
696 if let Ok(mut purl) = PackageUrl::new(PylockTomlParser::PACKAGE_TYPE.as_str(), name) {
697 if let Some(version) = version
698 && purl.with_version(version).is_err()
699 {
700 return None;
701 }
702 return Some(purl.to_string());
703 }
704
705 let mut purl = format!("pkg:pypi/{}", name);
706 if let Some(version) = version
707 && !version.is_empty()
708 {
709 purl.push('@');
710 purl.push_str(version);
711 }
712 Some(purl)
713}
714
715fn toml_value_to_json(value: &TomlValue) -> JsonValue {
716 match value {
717 TomlValue::String(value) => JsonValue::String(value.clone()),
718 TomlValue::Integer(value) => JsonValue::String(value.to_string()),
719 TomlValue::Float(value) => JsonValue::String(value.to_string()),
720 TomlValue::Boolean(value) => JsonValue::Bool(*value),
721 TomlValue::Datetime(value) => JsonValue::String(value.to_string()),
722 TomlValue::Array(values) => {
723 JsonValue::Array(values.iter().map(toml_value_to_json).collect())
724 }
725 TomlValue::Table(values) => JsonValue::Object(
726 values
727 .iter()
728 .map(|(key, value)| (key.clone(), toml_value_to_json(value)))
729 .collect::<JsonMap<String, JsonValue>>(),
730 ),
731 }
732}
733
734fn default_package_data() -> PackageData {
735 PackageData {
736 package_type: Some(PylockTomlParser::PACKAGE_TYPE),
737 primary_language: Some("Python".to_string()),
738 datasource_id: Some(DatasourceId::PypiPylockToml),
739 ..Default::default()
740 }
741}
742
743crate::register_parser!(
744 "pylock.toml lockfile",
745 &["**/pylock.toml", "**/pylock.*.toml"],
746 "pypi",
747 "Python",
748 Some("https://packaging.python.org/en/latest/specifications/pylock-toml/"),
749);