Public/New-PythonProject.ps1

function New-PythonProject {
    <#
    .SYNOPSIS
    Creates a runnable Python package project.

    .DESCRIPTION
    Creates a pyproject.toml-based src-layout package with a README, gitignore,
    package initializer, and pytest import test.

    .PARAMETER Name
    Project directory and distribution name.

    .PARAMETER Path
    Parent directory in which the project is created.

    .PARAMETER PackageName
    Import package name. Defaults to a normalized form of Name.

    .PARAMETER Description
    Project description used in pyproject.toml and README.md.

    .PARAMETER Force
    Allows an existing project directory and overwrites generated files.

    .EXAMPLE
    mkpyproj sample-app -PackageName sample_app
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    [Alias('mkpyproj')]
    [OutputType([System.IO.DirectoryInfo])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidatePattern('^[A-Za-z0-9][A-Za-z0-9._-]*$')]
        [string]$Name,

        [Parameter()]
        [string]$Path = (Get-Location).Path,

        [Parameter()]
        [string]$PackageName,

        [Parameter()]
        [string]$Description = 'Python project generated by DevXToolkit.',

        [switch]$Force
    )

    if ([string]::IsNullOrWhiteSpace($PackageName)) {
        $PackageName = ($Name -replace '[-.]', '_').ToLowerInvariant()
    }
    $PackageName = ConvertTo-DtPythonIdentifier -Name $PackageName -ParameterName 'PackageName'
    $projectRoot = Resolve-DtContainedPath -BasePath $Path -ChildName $Name

    if ((Test-Path -LiteralPath $projectRoot) -and -not $Force) {
        throw "Project already exists. Use -Force to update generated files: $projectRoot"
    }
    if (-not $PSCmdlet.ShouldProcess($projectRoot, 'Create Python project')) {
        return
    }

    $packageRoot = Join-Path -Path $projectRoot -ChildPath "src/$PackageName"
    $testsRoot = Join-Path -Path $projectRoot -ChildPath 'tests'
    New-DtDirectory -Path $projectRoot -Confirm:$false | Out-Null
    New-DtDirectory -Path $packageRoot -Confirm:$false | Out-Null
    New-DtDirectory -Path $testsRoot -Confirm:$false | Out-Null

    $safeTitle = ConvertTo-DtSafeMarkdownText -Text $Name
    $safeDescription = ConvertTo-DtSafeMarkdownText -Text $Description
    $tomlName = ConvertTo-DtTomlString -Text $Name
    $tomlDescription = ConvertTo-DtTomlString -Text $Description
    $pyproject = @"
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project]
name = "$tomlName"
version = "0.1.0"
description = "$tomlDescription"
requires-python = ">=3.9"

[tool.setuptools.packages.find]
where = ["src"]

[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
"@

    $readme = @"
# $safeTitle

$safeDescription

## Development

``````powershell
python -m pip install -e .
python -m pytest
``````
"@

    $gitignore = @"
__pycache__/
*.py[cod]
.pytest_cache/
.venv/
build/
dist/
*.egg-info/
"@

    $testContent = @"
def test_package_imports():
    import $PackageName

    assert $PackageName is not None
"@


    Set-DtFileContent -Path (Join-Path $projectRoot 'pyproject.toml') -Content $pyproject -Force:$Force -Confirm:$false | Out-Null
    Set-DtFileContent -Path (Join-Path $projectRoot 'README.md') -Content $readme -Force:$Force -Confirm:$false | Out-Null
    Set-DtFileContent -Path (Join-Path $projectRoot '.gitignore') -Content $gitignore -Force:$Force -Confirm:$false | Out-Null
    Set-DtFileContent -Path (Join-Path $packageRoot '__init__.py') -Content ('"""' + $PackageName + ' package."""') -Force:$Force -Confirm:$false | Out-Null
    Set-DtFileContent -Path (Join-Path $testsRoot 'test_import.py') -Content $testContent -Force:$Force -Confirm:$false | Out-Null

    Get-Item -LiteralPath $projectRoot
}